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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +434 -523
app.py CHANGED
@@ -1,572 +1,483 @@
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",
 
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",