Hamed744 commited on
Commit
89eadce
·
verified ·
1 Parent(s): eeeee10

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +477 -434
app.py CHANGED
@@ -1,483 +1,526 @@
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
 
 
 
 
 
 
 
 
 
 
3
  import mimetypes
4
+ import os
5
  import re
6
  import struct
7
+ import time
8
+ # import zipfile
9
+ from google import genai
10
+ from google.genai import types as genai_types
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ import logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14
+
15
+ try:
16
+ from pydub import AudioSegment
17
+ PYDUB_AVAILABLE = True
18
+ except ImportError:
19
+ PYDUB_AVAILABLE = False
20
+ logging.warning("Pydub (for audio merging) not found. Merging will be disabled if multiple audio chunks are generated.")
21
+
22
+ # --- START: YOUR EXACT CORE TTS LOGIC (AlphaTTS_Original) ---
23
+ SPEAKER_VOICES = [
24
+ "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
25
+ "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
26
+ "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib",
27
+ "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
28
+ "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
29
+ ]
30
+ FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
31
+ DEFAULT_MAX_CHUNK_SIZE = 3800
32
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
33
+ DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio"
34
+
35
+ def _log(message, log_list):
36
+ log_list.append(message)
37
+ logging.info(f"[AlphaTTS_LOG_INTERNAL] {message}")
38
+
39
+ def save_binary_file(file_name, data, log_list):
40
+ try:
41
+ with open(file_name, "wb") as f: f.write(data)
42
+ _log(f"✅ فایل ذخیره شد: {file_name}", log_list)
43
+ return file_name
44
+ except Exception as e:
45
+ _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
46
+ return None
47
+
48
+ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
49
+ parameters = parse_audio_mime_type(mime_type)
50
+ bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
51
+ num_channels, data_size = 1, len(audio_data)
52
+ bytes_per_sample, block_align = bits_per_sample // 8, num_channels * (bits_per_sample // 8)
53
+ byte_rate, chunk_size = rate * block_align, 36 + data_size
54
+ 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)
55
+ return header + audio_data
56
+
57
+ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
58
+ bits, rate = 16, 24000
59
+ for param in mime_type.split(";"):
60
+ param = param.strip()
61
+ if param.lower().startswith("rate="):
62
+ try: rate = int(param.split("=", 1)[1])
63
+ except ValueError: pass
64
+ elif param.startswith("audio/L"):
65
+ try: bits = int(param.split("L", 1)[1])
66
+ except ValueError: pass
67
+ return {"bits_per_sample": bits, "rate": rate}
68
+
69
+ def smart_text_split(text, max_size=3800, log_list=None):
70
+ if len(text) <= max_size: return [text]
71
+ chunks, current_chunk = [], ""
72
+ sentences = re.split(r'(?<=[.!?؟۔])\s+', text)
73
+ for sentence in sentences:
74
+ if len(current_chunk) + len(sentence) + 1 > max_size:
75
+ if current_chunk: chunks.append(current_chunk.strip())
76
+ current_chunk = sentence
77
+ while len(current_chunk) > max_size:
78
+ split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
79
+ part, current_chunk = (current_chunk[:split_idx+1], current_chunk[split_idx+1:]) if split_idx != -1 else (current_chunk[:max_size], current_chunk[max_size:])
80
+ chunks.append(part.strip())
81
+ else: current_chunk += (" " if current_chunk else "") + sentence
82
+ if current_chunk: chunks.append(current_chunk.strip())
83
+ final_chunks = [c for c in chunks if c]
84
+ if log_list: _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
85
+ return final_chunks
86
+
87
+ def merge_audio_files_func(file_paths, output_path, log_list):
88
+ if not PYDUB_AVAILABLE: _log("❌ pydub در دسترس نیست.", log_list); return False
89
+ try:
90
+ _log(f"🔗 ادغام {len(file_paths)} فایل صوتی...", log_list)
91
+ combined = AudioSegment.empty()
92
+ for i, fp in enumerate(file_paths):
93
+ if os.path.exists(fp):
94
+ try:
95
+ segment = AudioSegment.from_file(fp)
96
+ combined += segment
97
+ if i < len(file_paths) - 1:
98
+ combined += AudioSegment.silent(duration=150)
99
+ except Exception as e_pydub:
100
+ _log(f"⚠️ خطای Pydub در پردازش فایل '{fp}': {e_pydub}. این فایل نادیده گرفته می شود.", log_list)
101
+ continue
102
+ else: _log(f"⚠️ فایل پیدا نشد: {fp}", log_list)
103
+ if len(combined) == 0:
104
+ _log("❌ هیچ قطعه صوتی معتبری برای ادغام یافت نشد.", log_list)
105
+ return False
106
+ combined.export(output_path, format="wav")
107
+ _log(f"✅ فایل ادغام شده: {output_path}", log_list); return True
108
+ except Exception as e: _log(f"❌ خطا در ادغام: {e}", log_list); return False
109
+
110
+ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list):
111
+ output_base_name = DEFAULT_OUTPUT_FILENAME_BASE
112
+ max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
113
+ _log(f"🚀 شروع فرآیند با مدل: {FIXED_MODEL_NAME}...", log_list)
114
+ api_key = os.environ.get("GEMINI_API_KEY")
115
+ if not api_key: _log("❌ کلید API تنظیم نشده.", log_list); return None
116
+ try: client = genai.Client(api_key=api_key)
117
+ except Exception as e: _log(f"❌ خطا در کلاینت: {e}", log_list); return None
118
+ if not text_input or not text_input.strip(): _log("❌ متن ورودی خالی.", log_list); return None
119
+ text_chunks = smart_text_split(text_input, max_chunk, log_list)
120
+ if not text_chunks: _log("❌ متن قابل پردازش نیست.", log_list); return None
121
 
122
+ generated_files = []
123
+ for i, chunk in enumerate(text_chunks):
124
+ _log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)} (صدا: {selected_voice}, دما: {temperature_val})...", log_list)
125
+ final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
126
+ contents = [genai_types.Content(role="user", parts=[genai_types.Part.from_text(text=final_text)])]
127
+ config = genai_types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
128
+ speech_config=genai_types.SpeechConfig(voice_config=genai_types.VoiceConfig(
129
+ prebuilt_voice_config=genai_types.PrebuiltVoiceConfig(voice_name=selected_voice))))
130
+ fname_base = f"{output_base_name}_part{i+1:03d}"
131
  try:
132
+ response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
133
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
134
+ inline_data = response.candidates[0].content.parts[0].inline_data
135
+ data_buffer = inline_data.data
136
+ mime_type = inline_data.mime_type
137
+ _log(f"داده صوتی در candidate.part[0].inline_data برای قطعه {i+1} یافت شد. MIME: {mime_type}", log_list)
138
+ ext = mimetypes.guess_extension(mime_type) or ".wav"
139
+ if "audio/L" in mime_type and ext == ".wav":
140
+ _log(f"تبدیل صدای خام PCM (MIME: {mime_type}) به WAV برای قطعه {i+1}.", log_list)
141
+ data_buffer = convert_to_wav(data_buffer, mime_type)
142
+ if not ext.startswith("."): ext = "." + ext
143
+ fpath = save_binary_file(f"{fname_base}{ext}", data_buffer, log_list)
144
+ if fpath: generated_files.append(fpath)
145
+ else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی.", log_list)
146
+ except Exception as e: _log(f" خطا در تولید قطعه {i+1}: {e}", log_list); continue
147
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1:
148
+ _log(f"💤 توقف کوتاه ({sleep_time} ثانیه) قبل از قطعه بعدی...", log_list)
149
+ time.sleep(sleep_time)
150
+
151
+ if not generated_files: _log("❌ هیچ فایلی تولید نشد.", log_list); return None
152
+ _log(f"🎉 {len(generated_files)} فایل(های) صوتی تولید شد.", log_list)
153
 
154
+ final_audio_file = None
155
+ final_output_path_base = f"{output_base_name}_final"
156
 
157
+ if len(generated_files) > 1:
158
+ if PYDUB_AVAILABLE:
159
+ merged_fn = f"{final_output_path_base}.wav"
160
+ if os.path.exists(merged_fn):
161
+ try: os.remove(merged_fn)
162
+ except Exception as e_rm: _log(f"⚠️ خطا در حذف فایل ادغام شده قبلی '{merged_fn}': {e_rm}", log_list)
163
+ if merge_audio_files_func(generated_files, merged_fn, log_list):
164
+ final_audio_file = merged_fn
165
+ for fp in generated_files:
166
+ if os.path.abspath(fp) != os.path.abspath(merged_fn):
167
+ try: os.remove(fp)
168
+ except: pass
169
+ else:
170
+ if generated_files:
171
+ try:
172
+ source_path = generated_files[0]
173
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
174
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
175
+ if os.path.exists(target_path): os.remove(target_path)
176
+ os.rename(source_path, target_path)
177
+ final_audio_file = target_path
178
+ if final_audio_file == target_path:
179
+ for i_gf in range(1, len(generated_files)):
180
+ try: os.remove(generated_files[i_gf])
181
+ except: pass
182
+ except Exception as e_rename:
183
+ _log(f"خطا در تغییر نام فایل اولین قطعه: {e_rename}", log_list)
184
+ final_audio_file = generated_files[0]
185
+ else:
186
+ _log("⚠️ pydub نیس��. اولین قطعه ارائه می‌شود.", log_list)
187
+ if generated_files:
188
+ try:
189
+ source_path = generated_files[0]
190
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
191
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
192
+ if os.path.exists(target_path): os.remove(target_path)
193
+ os.rename(source_path, target_path)
194
+ final_audio_file = target_path
195
+ if final_audio_file == target_path:
196
+ for i_gf in range(1, len(generated_files)):
197
+ try: os.remove(generated_files[i_gf])
198
+ except: pass
199
+ except Exception as e_rename_single:
200
+ _log(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}", log_list)
201
+ final_audio_file = generated_files[0]
202
+ elif len(generated_files) == 1:
203
+ try:
204
+ source_path = generated_files[0]
205
+ target_path = f"{final_output_path_base}{os.path.splitext(source_path)[1]}"
206
+ if os.path.abspath(source_path) != os.path.abspath(target_path):
207
+ if os.path.exists(target_path): os.remove(target_path)
208
+ os.rename(source_path, target_path)
209
+ final_audio_file = target_path
210
+ except Exception as e_rename_single_final:
211
+ _log(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single_final}", log_list)
212
+ final_audio_file = generated_files[0]
213
+
214
+ if final_audio_file and not os.path.exists(final_audio_file):
215
+ _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!", log_list)
216
+ return None
217
+ return final_audio_file
218
 
219
+ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
220
+ logs = []
221
+ actual_text = ""
222
+ if use_file_input:
223
+ if uploaded_file:
224
+ try:
225
+ with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
226
+ if not actual_text: return None
227
+ except Exception as e: _log(f"❌ خطا خواندن فایل: {e}", logs); return None
228
+ else: return None
229
+ else:
230
+ actual_text = text_to_speak
231
+ if not actual_text or not actual_text.strip(): return None
232
 
233
+ final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, logs)
234
+ # for log_entry in logs: print(log_entry)
235
+ return final_path
236
+ # --- END: YOUR EXACT CORE TTS LOGIC (AlphaTTS_Original) ---
237
 
 
 
 
 
 
 
238
 
239
+ # --- START: Styling and UI (Applying AlphaTranslator_Styled look to YOUR UI structure) ---
240
+ FLY_PRIMARY_COLOR_HEX = "#4F46E5"
241
+ FLY_SECONDARY_COLOR_HEX = "#10B981"
242
+ FLY_ACCENT_COLOR_HEX = "#D97706"
243
+ FLY_TEXT_COLOR_HEX = "#1F2937"
244
+ FLY_SUBTLE_TEXT_HEX = "#6B7280"
245
+ FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
246
+ FLY_WHITE_HEX = "#FFFFFF"
247
+ FLY_BORDER_COLOR_HEX = "#D1D5DB"
248
+ FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6"
249
+
250
+ app_theme_applied_styled = gr.themes.Base(
251
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
252
+ ).set(
253
+ body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
254
+ )
255
+
256
+ # Corrected CSS: Using string literals for elem_ids
257
+ final_combined_css_v3 = f"""
258
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
259
  @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
260
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
261
+
262
  :root {{
263
+ --fly-primary: {FLY_PRIMARY_COLOR_HEX};
264
+ --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
265
+ --fly-accent: {FLY_ACCENT_COLOR_HEX};
266
+ --fly-text-primary: {FLY_TEXT_COLOR_HEX};
267
+ --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX};
268
+ --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
269
+ --fly-bg-white: {FLY_WHITE_HEX};
270
+ --fly-border-color: {FLY_BORDER_COLOR_HEX};
271
+ --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE};
272
+ --fly-primary-rgb: 79,70,229;
273
+ --fly-accent-rgb: 217,119,6;
274
+
275
+ --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem;
276
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);
277
+ --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1);
278
+
279
+ --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
280
+ --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
281
+
282
+ --app-button-bg-original: #2979FF;
283
+ --radius-input-original: 8px;
284
  }}
285
+
286
+ body {{
287
+ font-family: var(--font-global);
288
  direction: rtl;
289
+ background-color: var(--fly-bg-light) !important;
290
+ color: var(--fly-text-primary);
291
+ line-height: 1.7;
292
+ font-size: 16px;
293
  }}
294
+
295
+ .gradio-container {{
296
+ max-width:100% !important; width:100% !important; min-height:100vh;
297
+ margin:0 auto !important; padding:0 !important; border-radius:0 !important;
298
+ box-shadow:none !important; background:linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%) !important;
299
+ display:flex; flex-direction:column;
 
300
  }}
301
+
302
+ .app-header-alphatts-v2 {{
303
+ text-align:center; padding:2.5rem 1rem; margin:0;
304
+ background:linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%);
305
+ color:var(--fly-bg-white); border-bottom-left-radius:var(--radius-xl);
306
+ border-bottom-right-radius:var(--radius-xl); box-shadow:var(--shadow-lg);
307
+ position:relative; overflow:hidden;
 
308
  }}
309
+ .app-header-alphatts-v2::before {{
310
+ content:''; position:absolute; top:-50px; right:-50px; width:150px; height:150px;
311
+ background:rgba(255,255,255,0.1); border-radius:9999px;
312
+ opacity:0.5; transform:rotate(45deg);
 
313
  }}
314
+ .app-header-alphatts-v2 h1 {{
315
+ font-size:2.25em !important; font-weight:800 !important; margin:0 0 0.5rem 0;
316
+ font-family:var(--font-english); letter-spacing:-0.5px; text-shadow:0 2px 4px rgba(0,0,0,0.1);
 
 
317
  }}
318
+ .app-header-alphatts-v2 p {{
319
+ font-size:1em !important; margin-top:0.25rem; font-weight:400;
320
+ color:rgba(255,255,255,0.85) !important;
 
 
 
 
 
 
 
321
  }}
322
+
323
+ .main-content-area-alphatts-v2 {{
324
+ flex-grow:1; padding:0.75rem; width:100%; margin:0 auto; box-sizing:border-box;
 
 
 
 
 
 
 
 
 
325
  }}
326
+ .content-panel-alphatts-v2 {{
327
+ background-color:var(--fly-bg-white); padding:1rem; border-radius:var(--radius-xl);
328
+ box-shadow:var(--shadow-xl); margin-top:-2rem; position:relative; z-index:10;
329
+ margin-bottom:2rem; width:100%; box-sizing:border-box;
 
 
 
 
 
 
 
 
 
 
330
  }}
331
+
332
+ /* Styling YOUR UI elements using their elem_ids */
333
+ .content-panel-alphatts-v2 #text_input_main_alpha_v3 textarea, /* Textbox for text_to_speak_tb */
334
+ .content-panel-alphatts-v2 #speech_prompt_alpha_v3 textarea, /* Textbox for speech_prompt_tb */
335
+ .content-panel-alphatts-v2 #speaker_voice_alpha_v3 input, /* Dropdown input */
336
+ .content-panel-alphatts-v2 #speaker_voice_alpha_v3 select, /* Dropdown select */
337
+ .content-panel-alphatts-v2 #file_uploader_alpha_main_v3 .upload-button, /* File input button part */
338
+ .content-panel-alphatts-v2 #temperature_slider_alpha_v3 input[type="range"]
339
+ {{
340
+ border-radius:var(--radius-input-original) !important;
341
+ border:1.5px solid var(--fly-border-color) !important;
342
+ font-size:0.95em !important;
343
+ background-color:var(--fly-input-bg-simple) !important;
344
+ padding:10px 12px !important;
345
+ color:var(--fly-text-primary) !important;
346
+ box-shadow: none !important;
347
  }}
348
+
349
+ /* Focus styles for inputs by elem_id */
350
+ .content-panel-alphatts-v2 #text_input_main_alpha_v3 textarea:focus,
351
+ .content-panel-alphatts-v2 #speech_prompt_alpha_v3 textarea:focus,
352
+ .content-panel-alphatts-v2 #speaker_voice_alpha_v3 input:focus,
353
+ .content-panel-alphatts-v2 #speaker_voice_alpha_v3 select:focus,
354
+ .content-panel-alphatts-v2 #file_uploader_alpha_main_v3 .upload-button:focus-within
355
+ {{
356
+ border-color:var(--fly-primary) !important;
357
+ box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;
358
+ background-color:var(--fly-bg-white) !important;
359
  }}
360
+ .content-panel-alphatts-v2 #file_uploader_alpha_main_v3 .upload-button {{ text-align:center; border-style: dashed !important; }} /* If file input has a button class */
361
+ .content-panel-alphatts-v2 #file_uploader_alpha_main_v3 > div {{ text-align:center; border-style: dashed !important; border-radius:var(--radius-input-original) !important; border-width: 1.5px !important; border-color: var(--fly-border-color) !important; background-color:var(--fly-input-bg-simple) !important; padding: 10px 12px !important; }} /* General file div */
362
+
363
+
364
+ /* Button: Targeting YOUR button by its elem_id */
365
+ .content-panel-alphatts-v2 .gr-button#generate_button_alpha_v3
366
+ {{
367
+ background:var(--fly-accent) !important;
368
+ margin-top:1.5rem !important; padding:12px 20px !important;
369
+ transition:all 0.25s ease-in-out !important; color:white !important; font-weight:600 !important;
370
+ border-radius:var(--radius-input-original) !important; border:none !important;
371
+ box-shadow:0 3px 8px -1px rgba(var(--fly-accent-rgb),0.3) !important;
372
+ width:100% !important; font-size:1.05em !important;
373
+ display:flex; align-items:center; justify-content:center;
374
  }}
375
+ .content-panel-alphatts-v2 .gr-button#generate_button_alpha_v3:hover
376
+ {{
377
+ background:#B45309 !important; transform:translateY(-1px) !important;
378
+ box-shadow:0 5px 10px -1px rgba(var(--fly-accent-rgb),0.4) !important;
 
379
  }}
380
+
381
+ /* Labels */
382
+ .content-panel-alphatts-v2 .gr-form label > .label-text span,
383
+ .content-panel-alphatts-v2 .gr-form .gr-input-label > label > .label-text span /* More specific for Gradio 3.x+ labels */
384
+ {{
385
+ font-weight:500 !important; color: var(--fly-text-secondary) !important;
386
+ font-size:0.88em !important; margin-bottom:6px !important; display:inline-block;
387
+ }}
388
+ .content-panel-alphatts-v2 .temp_description_class_alpha_v3 {{
389
+ font-size: 0.85em; color: var(--fly-text-secondary); margin-top: -0.4rem; margin-bottom: 1rem;
390
+ }}
391
+
392
+ .content-panel-alphatts-v2 #output_audio_player_alpha_v3 audio
393
+ {{
394
+ width: 100%; border-radius: var(--radius-input-original); margin-top:0.8rem;
395
  }}
396
+
397
+ .content-panel-alphatts-v2 .gr-examples .gr-button.gr-button-tool,
398
+ .content-panel-alphatts-v2 .gr-examples .gr-sample-button
399
+ {{
400
+ background-color:#E0E7FF !important; color:var(--fly-primary) !important;
401
+ border-radius:var(--radius-sm) !important; font-size:0.78em !important; padding:4px 8px !important;
402
+ }}
403
+ .content-panel-alphatts-v2 .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
404
+
405
+ .app-footer-alphatts-v2 {{
406
+ text-align:center;font-size:0.85em;color:var(--fly-text-secondary);margin-top:2.5rem;
407
+ padding:1rem 0;background-color:rgba(255,255,255,0.3);backdrop-filter:blur(5px);
408
+ border-top:1px solid var(--fly-border-color);
409
  }}
410
+ footer, .gradio-footer, .flagging-container, .footer-utils {{
411
+ display:none !important; visibility:hidden !important;
 
 
 
 
 
 
 
 
 
 
412
  }}
413
+
414
+ @media (min-width:640px) {{
415
+ .main-content-area-alphatts-v2 {{padding:1.5rem;max-width:700px;}}
416
+ .content-panel-alphatts-v2 {{padding:1.5rem;}}
417
+ .app-header-alphatts-v2 h1 {{font-size:2.5em !important;}}
418
+ .app-header-alphatts-v2 p {{font-size:1.05em !important;}}
 
 
419
  }}
420
+ @media (min-width:768px) {{
421
+ .main-content-area-alphatts-v2 {{max-width:780px;}}
422
+ .content-panel-alphatts-v2 {{padding:2rem;}}
423
+ .content-panel-alphatts-v2 .gr-button#generate_button_alpha_v3
424
+ {{
425
+ width:auto !important; align-self:flex-start;
426
+ }}
427
+ .app-header-alphatts-v2 h1 {{font-size:2.75em !important;}}
428
+ .app-header-alphatts-v2 p {{font-size:1.1em !important;}}
429
  }}
430
  """
431
 
432
+ # --- Gradio UI Definition (YOUR UI structure, with new CSS applied) ---
433
+ with gr.Blocks(theme=app_theme_applied_styled, css=final_combined_css_v3, title=f"آلفا TTS ({FIXED_MODEL_NAME.split('-')[1]})") as demo:
434
+ gr.HTML(f"""
435
+ <div class='app-header-alphatts-v2'>
436
+ <h1>🚀 Alpha TTS</h1>
437
+ <p>جادوی تبدیل متن به صدا در دستان شما (Gemini {FIXED_MODEL_NAME.split('-')[1]})</p>
438
+ </div>
 
 
 
 
 
 
 
 
439
  """)
440
 
441
+ with gr.Column(elem_classes=["main-content-area-alphatts-v2"]):
442
+ with gr.Column(elem_classes=["content-panel-alphatts-v2"]): # This wraps YOUR UI elements
443
+
444
+ if not os.environ.get("GEMINI_API_KEY"):
445
+ gr.Markdown("<p style='color:red; text-align:center; margin-bottom:1rem;'>⚠️ <b>هشدار:</b> متغیر محیطی GEMINI_API_KEY تنظیم نشده است. برنامه کار نخواهد کرد.</p>")
446
+
447
+ # YOUR ORIGINAL UI LAYOUT
448
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
449
+
450
+ uploaded_file_input = gr.File(
451
+ label=" ",
452
+ file_types=['.txt'],
453
+ visible=False,
454
+ elem_id="file_uploader_alpha_main_v3" # YOUR ID
455
+ )
456
+
457
+ text_to_speak_tb = gr.Textbox(
458
+ label="متن فارسی برای تبدیل",
459
+ placeholder="مثال: سلام، فردا هوا چطور است؟",
460
+ lines=5,
461
+ value="",
462
+ visible=True,
463
+ elem_id="text_input_main_alpha_v3" # YOUR ID
464
+ )
465
+
466
+ use_file_input_cb.change(
467
+ fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)),
468
+ inputs=use_file_input_cb,
469
+ outputs=[uploaded_file_input, text_to_speak_tb]
470
+ )
471
+
472
+ speech_prompt_tb = gr.Textbox(
473
+ label="سبک گفتار (اختیاری)",
474
+ placeholder="مثال: با لحنی شاد و پرانرژی",
475
+ value="با لحنی دوستانه و رسا صحبت کن.",
476
+ lines=2, elem_id="speech_prompt_alpha_v3" # YOUR ID
477
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ speaker_voice_dd = gr.Dropdown(
480
+ SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3" # YOUR ID
481
+ )
 
 
482
 
483
+ temperature_slider = gr.Slider(
484
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
485
+ elem_id="temperature_slider_alpha_v3" # YOUR ID
486
+ )
487
+ gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
488
+
489
+ generate_button = gr.Button("🚀 تولید و پخش صدا", elem_id="generate_button_alpha_v3") # YOUR ID
490
 
491
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3") # YOUR ID
492
+
493
+ gr.HTML("<hr class='custom-hr'>")
494
+ gr.Markdown(
495
+ "<h3 style='text-align:center; font-weight:500; color:var(--fly-text-secondary); margin-top:1.5rem; margin-bottom:1rem;'>نمونه‌های کاربردی</h3>"
496
+ )
497
+ gr.Examples(
498
+ examples=[
499
+ [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
500
+ [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
501
+ ],
502
+ inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
503
+ outputs=[output_audio],
504
+ fn=gradio_tts_interface,
505
+ cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true"
506
+ )
507
 
508
+ 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>")
509
 
510
+ if generate_button is not None:
511
+ generate_button.click(
512
+ fn=gradio_tts_interface,
513
+ 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)],
514
+ outputs=[output_audio]
515
+ )
516
+ else:
517
+ logging.error("دکمه تولید صدا (generate_button_alpha_v3) در UI یافت نشد.")
518
 
 
519
  if __name__ == "__main__":
520
+ if not PYDUB_AVAILABLE:
521
+ logging.warning("Pydub (for audio merging) not found. Merging will be disabled if multiple audio chunks are generated.")
522
+ if not os.environ.get("GEMINI_API_KEY"):
523
+ logging.warning("GEMINI_API_KEY environment variable not set. TTS functionality WILL FAIL.")
 
 
524
 
525
  demo.launch(
526
  server_name="0.0.0.0",