Opera10 commited on
Commit
7c3976d
·
verified ·
1 Parent(s): 1a19a22

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -603
app.py CHANGED
@@ -1,630 +1,167 @@
1
- import gradio as gr
2
- import edge_tts
3
- import tempfile
4
- import asyncio
5
- import traceback
6
  import os
7
- import logging
8
  import time
 
 
9
  import threading
10
- import sys
11
-
12
- # کتابخانه ترجمه با گوگل
 
 
 
13
  from deep_translator import GoogleTranslator
 
14
 
15
- # --- START: پیکربندی لاگینگ ---
16
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
17
- # --- END: پیکربندی لاگینگ ---
 
18
 
19
- # --- START: تابع ری‌استارت خودکار دون تغییر) ---
20
- def auto_restart_service():
21
- RESTART_INTERVAL_SECONDS = 24 * 60 * 60
22
- logging.info(f"سرویس برای ری‌استارت خودکار پس از {RESTART_INTERVAL_SECONDS / 3600:.0f} ساعت زمان‌بندی شده است.")
23
- time.sleep(RESTART_INTERVAL_SECONDS)
24
- logging.info(f"زمان ری‌استارت خودکار فرا رسیده است. برنامه برای ری‌استارت خارج می‌شود...")
25
- os._exit(1)
26
- # --- END: تابع ری‌استارت خودکار ---
 
 
 
 
27
 
28
- # --- دیکشنری صداهای انگلیسی قدیمی (حفظ شده جهت سازگاری عقب‌رو API) ---
29
- language_dict_persian_keys = {
30
- 'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
31
- 'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
32
- 'انگلیسی (آمریکا) - کریستوفر (مرد)': 'en-US-ChristopherNeural', 'انگلیسی (آمریکا) - اریک (مرد)': 'en-US-EricNeural',
33
- 'انگلیسی (آمریکا) - میشل (زن)': 'en-US-MichelleNeural', 'انگلیسی (آمریکا) - راجر (مرد)': 'en-US-RogerNeural',
34
- 'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural', 'انگلیسی (بریتانیا) - میزی (زن)': 'en-GB-MaisieNeural',
35
- 'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural', 'انگلیسی (بریتانیا) - سونیا (زن)': 'en-GB-SoniaNeural',
36
- 'انگلیسی (بریتانیا) - توماس (مرد)': 'en-GB-ThomasNeural', 'انگلیسی (بریتانیا) - میا (زن، جدید)': 'en-GB-MiaNeural',
37
- 'انگلیسی (استرالیا) - ناتاشا (زن)': 'en-AU-NatashaNeural', 'انگلیسی (استرالیا) - ویلیام (مرد)': 'en-AU-WilliamNeural',
38
- 'انگلیسی (کانادا) - کلارا (زن)': 'en-CA-ClaraNeural', 'انگلیسی (کانادا) - لیام (مرد)': 'en-CA-LiamNeural',
39
- 'انگلیسی (ایرلند) - امیلی (زن)': 'en-IE-EmilyNeural', 'انگلیسی (ایرلند) - کانر (مرد)': 'en-IE-ConnorNeural',
40
- 'انگلیسی (هند) - نیرجا (زن)': 'en-IN-NeerjaNeural', 'انگلیسی (هند) - پرابهات (مرد)': 'en-IN-PrabhatNeural',
41
- 'انگلیسی (آفریقای جنوب) - لیا (زن)': 'en-ZA-LeahNeural', 'انگلیسی (آفریقای جنوبی) - لوک (مرد)': 'en-ZA-LukeNeural',
42
- }
43
 
44
- # --- پایگاه داده جدید زبان‌ها و صداهای پشتیبانی شده ---
45
  LANGUAGES_MAP = {
46
- "انگلیسی": {
47
- "code": "en",
48
- "voices": {
49
- 'زن - جنی مریکا)': 'en-US-JennyNeural',
50
- 'مرد - گای (آمریکا)': 'en-US-GuyNeural',
51
- 'زن - آنا مریکا - کودک)': 'en-US-AnaNeural',
52
- 'زن - آریا (آمریکا)': 'en-US-AriaNeural',
53
- 'مرد - کریستوفر (آمریکا)': 'en-US-ChristopherNeural',
54
- 'مرد - اریک مریکا)': 'en-US-EricNeural',
55
- 'زن - میشل مریکا)': 'en-US-MichelleNeural',
56
- 'مرد - راجر مریکا)': 'en-US-RogerNeural',
57
- 'زن - لیبی ریتانیا)': 'en-GB-LibbyNeural',
58
- 'زن - میزی ریتانیا)': 'en-GB-MaisieNeural',
59
- 'مرد - رایان (بریتانیا)': 'en-GB-RyanNeural',
60
- 'زن - سونیا ریتانیا)': 'en-GB-SoniaNeural',
61
- 'مرد - توماس (بریتانیا)': 'en-GB-ThomasNeural',
62
- 'زن - میا (بریتانیا)': 'en-GB-MiaNeural',
63
- 'زن - ناتاشا (استرالیا)': 'en-AU-NatashaNeural',
64
- 'مرد - ویلیام (استرالیا)': 'en-AU-WilliamNeural',
65
- 'زن - کلارا (کانادا)': 'en-CA-ClaraNeural',
66
- 'مرد - لیام (کانادا)': 'en-CA-LiamNeural',
67
- }
68
- },
69
- "فارسی": {
70
- "code": "fa",
71
- "voices": {
72
- 'زن - دلارا': 'fa-IR-DilaraNeural',
73
- 'مرد - فرید': 'fa-IR-FaridNeural'
74
- }
75
- },
76
- "عربی": {
77
- "code": "ar",
78
- "voices": {
79
- 'زن - فاطمه (امارات)': 'ar-AE-FatimaNeural',
80
- 'مرد - حمدان (امارات)': 'ar-AE-HamdanNeural',
81
- 'زن - امینه (الجزایر)': 'ar-DZ-AminaNeural',
82
- 'مرد - اسماعیل (الجزایر)': 'ar-DZ-IsmaelNeural',
83
- 'زن - سلما (مصر)': 'ar-EG-SalmaNeural',
84
- 'مرد - شاکر (مصر)': 'ar-EG-ShakirNeural',
85
- 'مرد - حامد (عربستان)': 'ar-SA-HamedNeural',
86
- 'زن - زاریه (عربستان)': 'ar-SA-ZariyahNeural',
87
- }
88
- },
89
- "ترکی": {
90
- "code": "tr",
91
- "voices": {
92
- 'زن - امل': 'tr-TR-EmelNeural',
93
- 'مرد - احمد': 'tr-TR-AhmetNeural',
94
- }
95
- },
96
- "فرانسوی": {
97
- "code": "fr",
98
- "voices": {
99
- 'زن - الویز (فرانسه)': 'fr-FR-EloiseNeural',
100
- 'مرد - هنری (فرانسه)': 'fr-FR-HenriNeural',
101
- 'زن - دنیز (فرانسه)': 'fr-FR-DeniseNeural',
102
- 'زن - سیلویا (کانادا)': 'fr-CA-SylvieNeural',
103
- 'مرد - ژان (کانادا)': 'fr-CA-JeanNeural',
104
- }
105
- },
106
- "آلمانی": {
107
- "code": "de",
108
- "voices": {
109
- 'زن - کاترین (آلمان)': 'de-DE-KatjaNeural',
110
- 'مرد - کنراد (آلمان)': 'de-DE-ConradNeural',
111
- 'زن - آمالا (آلمان)': 'de-DE-AmalaNeural',
112
- 'زن - اینگرید (اتریش)': 'de-AT-IngridNeural',
113
- 'مرد - جوناس (اتریش)': 'de-AT-JonasNeural',
114
- }
115
- },
116
- "اسپانیایی": {
117
- "code": "es",
118
- "voices": {
119
- 'زن - الویرا (اسپانیا)': 'es-ES-ElviraNeural',
120
- 'مرد - آلوارو (اسپانیا)': 'es-ES-AlvaroNeural',
121
- 'زن - دالیا (مکزیک)': 'es-MX-DaliaNeural',
122
- 'مرد - خورخه (مکزیک)': 'es-MX-JorgeNeural',
123
- }
124
- },
125
- "روسی": {
126
- "code": "ru",
127
- "voices": {
128
- 'زن - سوتلانا': 'ru-RU-SvetlanaNeural',
129
- 'مرد - دیمیتری': 'ru-RU-DmitryNeural',
130
- }
131
- },
132
- "چینی": {
133
- "code": "zh-CN",
134
- "voices": {
135
- 'زن - شیائوشیا (چین)': 'zh-CN-XiaoxiaNeural',
136
- 'مرد - یونشی (چین)': 'zh-CN-YunxiNeural',
137
- 'مرد - یونجیان (چین)': 'zh-CN-YunjianNeural',
138
- 'زن - شیائویی (چین)': 'zh-CN-XiaoyiNeural',
139
- }
140
- },
141
- "ژاپنی": {
142
- "code": "ja",
143
- "voices": {
144
- 'زن - نانامی': 'ja-JP-NanamiNeural',
145
- 'مرد - کیتا': 'ja-JP-KeitaNeural',
146
- 'زن - آئویی': 'ja-JP-AoiNeural',
147
- }
148
- },
149
- "ایتالیایی": {
150
- "code": "it",
151
- "voices": {
152
- 'زن - السا': 'it-IT-ElsaNeural',
153
- 'زن - ایزابلا': 'it-IT-IsabellaNeural',
154
- 'مرد - دیگو': 'it-IT-DiegoNeural',
155
- }
156
- },
157
- "کره‌ای": {
158
- "code": "ko",
159
- "voices": {
160
- 'زن - سونوهی': 'ko-KR-SunHiNeural',
161
- 'مرد - اینجون': 'ko-KR-InJoonNeural',
162
- }
163
- },
164
- "هندی": {
165
- "code": "hi",
166
- "voices": {
167
- 'زن - مادور': 'hi-IN-MadhurNeural',
168
- 'مرد - سوآرا': 'hi-IN-SwaraNeural',
169
- }
170
- },
171
- "هلندی": {
172
- "code": "nl",
173
- "voices": {
174
- 'زن - کوئلت': 'nl-NL-ColetteNeural',
175
- 'زن - فنو': 'nl-NL-FennaNeural',
176
- 'مرد - مارتین': 'nl-NL-MaartenNeural',
177
- }
178
- },
179
- "سوئدی": {
180
- "code": "sv",
181
- "voices": {
182
- 'زن - سوفی': 'sv-SE-SofieNeural',
183
- 'مرد - ماتیاس': 'sv-SE-MattiasNeural',
184
- }
185
- },
186
- "لهستانی": {
187
- "code": "pl",
188
- "voices": {
189
- 'زن - زوفیا': 'pl-PL-ZofiaNeural',
190
- 'مرد - مارک': 'pl-PL-MarekNeural',
191
- }
192
- },
193
- "پرتغالی": {
194
- "code": "pt",
195
- "voices": {
196
- 'زن - فرانسیسکا (برزیل)': 'pt-BR-FranciscaNeural',
197
- 'مرد - آنتونیو (برزیل)': 'pt-BR-AntonioNeural',
198
- 'زن - راکل (پرتغال)': 'pt-PT-RaquelNeural',
199
- 'مرد - دوآرته (پرتغال)': 'pt-PT-DuarteNeural',
200
- }
201
- }
202
  }
203
 
204
- # لیست زبان‌ها برای بخش رابط کاربری
205
- SOURCE_LANGUAGES = ["شناسایی خودکار"] + list(LANGUAGES_MAP.keys())
206
- TARGET_LANGUAGES = list(LANGUAGES_MAP.keys())
207
-
208
-
209
- # --- تابع پایه ترجمه گوگل ---
210
- async def translate_text_google_async(text_to_translate, source_language="auto", target_language="en"):
211
- if not text_to_translate or not text_to_translate.strip():
212
- return "خطا: متنی برای ترجمه وارد نشده است.", None
213
 
214
- def _translate():
215
- try:
216
- logging.info(f"شروع ترجمه متن: '{text_to_translate[:30]}...' از '{source_language}' به '{target_language}'")
217
- translated = GoogleTranslator(source=source_language, target=target_language).translate(text_to_translate)
218
- logging.info("ترجمه با Google Translate موفق بود.")
219
- return translated
220
- except Exception as e:
221
- logging.error(f"خطا در حین ��رجمه با Google: {e}\n{traceback.format_exc()}")
222
- return None
223
 
 
 
 
 
224
  try:
225
- translated_text = await asyncio.to_thread(_translate)
226
- if translated_text:
227
- return "ترجمه موفق", translated_text
228
- else:
229
- return "خطا: مشکلی در فرآیند ترجمه پیش آمد.", None
230
  except Exception as e:
231
- logging.error(f"خطای غیرمنتظره در فراخوانی ترد ترجمه: {e}\n{traceback.format_exc()}")
232
- return "خطای غیرمنتظره: مشکلی در سیستم ترجمه رخ داد.", None
233
-
234
-
235
- # --- تابع جدید تولید صدا چند زبانه ---
236
- async def text_to_speech_multi_async(text_to_speak, target_lang_key, tts_voice_key, rate, volume, pitch):
237
- lang_info = LANGUAGES_MAP.get(target_lang_key)
238
- if not lang_info:
239
- return f"خطای TTS: زبان مقصد '{target_lang_key}' معتبر نیست.", None
240
-
241
- voice_id = lang_info["voices"].get(tts_voice_key)
242
- if not voice_id:
243
- # دریافت گوینده پیش‌فرض در صورت نبود تطابق مستقیم
244
- if lang_info["voices"]:
245
- voice_id = list(lang_info["voices"].values())[0]
246
- else:
247
- return f"خطای TTS: صدایی برای زبان '{target_lang_key}' تعریف نشده است.", None
248
 
249
- if not text_to_speak or not text_to_speak.strip():
250
- return "خطای TTS: متن ترجمه شده برای خواندن خالی است.", None
251
-
252
- logging.info(f"TTS: شروع تولید صدا برای '{voice_id}'...")
253
- tmp_path = None
254
  try:
255
- rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
256
- communicate = edge_tts.Communicate(text_to_speak, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
257
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
258
- tmp_path = tmp_file.name
259
- await communicate.save(tmp_path)
260
- logging.info(f"TTS: صدا با موفقیت در '{os.path.basename(tmp_path)}' ذخیره شد.")
261
- return "TTS موفق", tmp_path
262
  except Exception as e:
263
- if tmp_path and os.path.exists(tmp_path):
264
- try: os.remove(tmp_path)
265
- except Exception as e_rem: logging.warning(f"TTS: خطای پاک کردن فایل موقت: {e_rem}")
266
- error_type = type(e).__name__
267
- logging.error(f"TTS: خطای نامشخص برای '{voice_id}': {error_type} - {e}")
268
- return f"خطای TTS ({error_type}): مشکلی در تولید صدا پیش آمد.", None
269
-
270
-
271
- # --- تابع هماهنگ‌کننده چند زبانه جدید برای رابط کاربری اصلی ---
272
- async def translate_and_speak_multi_wrapper(text, src_lang_persian, target_lang_persian, voice_key, rate, volume, pitch):
273
- if not text or not text.strip():
274
- return "لطفاً متنی برای ترجمه وارد کنید.", None
275
-
276
- # استخراج کدهای زبان
277
- src_code = "auto" if src_lang_persian == "شناسایی خودکار" else LANGUAGES_MAP.get(src_lang_persian, {}).get("code", "auto")
278
- target_code = LANGUAGES_MAP.get(target_lang_persian, {}).get("code", "en")
279
-
280
- translation_status_msg, translated_text = await translate_text_google_async(text, src_code, target_code)
281
-
282
- if not translated_text:
283
- return translation_status_msg, None
284
-
285
- tts_status_msg, audio_path = await text_to_speech_multi_async(translated_text, target_lang_persian, voice_key, rate, volume, pitch)
286
-
287
- if not audio_path:
288
- return f"{translated_text}\n\n({tts_status_msg})", None
289
-
290
- return translated_text, audio_path
291
-
292
-
293
- # --- تابع قدیمی جهت حفظ سازگاری کامل با APIهای از قبل متصل شده (بدون تغییر امضا) ---
294
- async def translate_and_speak_async_wrapper(persian_text, english_tts_voice_key, rate, volume, pitch):
295
- if not persian_text or not persian_text.strip():
296
- return "لطفاً متن فارسی را برای ترجمه وارد کنید.", None
297
-
298
- # ترجمه از فارسی به انگلیسی با مترجم گوگل
299
- translation_status_msg, translated_text = await translate_text_google_async(persian_text, source_language="fa", target_language="en")
300
-
301
- if not translated_text:
302
- return translation_status_msg, None
303
-
304
- translated_text_output = translated_text
305
 
306
- # پیدا کردن شناسه صوتی انگلیسی از دیکشنری قبلی
307
- voice_id = language_dict_persian_keys.get(english_tts_voice_key)
308
  if not voice_id:
309
- if language_dict_persian_keys:
310
- voice_id = list(language_dict_persian_keys.values())[0]
311
- else:
312
- return f"{translated_text_output}\n\n(خطای TTS: هیچ صدایی موجود نیست.)", None
313
-
314
- logging.info(f"TTS (Legacy API): شروع تولید صدا برای '{voice_id}'...")
315
- tmp_path = None
316
- try:
317
- rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
318
- communicate = edge_tts.Communicate(translated_text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
319
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
320
- tmp_path = tmp_file.name
321
- await communicate.save(tmp_path)
322
- return translated_text_output, tmp_path
323
- except Exception as e:
324
- if tmp_path and os.path.exists(tmp_path):
325
- try: os.remove(tmp_path)
326
- except Exception as e_rem: pass
327
- return f"{translated_text_output}\n\n(خطای TTS: امکان تولید صدای این بخش میسر نشد.)", None
328
 
 
 
 
 
329
 
330
- # --- تابع کمکی تغییر داینامیک لیست گویندگان در رابط کاربری ---
331
- def handle_target_lang_change(target_lang_persian):
332
- lang_info = LANGUAGES_MAP.get(target_lang_persian)
333
- if not lang_info or not lang_info["voices"]:
334
- return gr.Dropdown(choices=[], value=None, label="🗣️ گوینده‌ای یافت نشد", interactive=False)
335
 
336
- choices = list(lang_info["voices"].keys())
337
- return gr.Dropdown(choices=choices, value=choices[0], label="🗣️ انتخاب گوینده و جنسیت صدا", interactive=True)
338
-
339
-
340
- # --- بخش طراحی رابط کاربری (پوسته مدرن، جذاب و واکنش‌گرا) ---
341
- FLY_PRIMARY_COLOR_HEX = "#4F46E5" # نیلی تیره
342
- FLY_SECONDARY_COLOR_HEX = "#0EA5E9" # آبی آسمانی روشن
343
- FLY_ACCENT_COLOR_HEX = "#D97706" # نارنجی پررنگ
344
- FLY_TEXT_COLOR_HEX = "#1F2937" # توسی تیره
345
- FLY_SUBTLE_TEXT_HEX = "#4B5563" # توسی متوسط
346
- FLY_LIGHT_BACKGROUND_HEX = "#F8FAFC" # پس‌زمینه بسیار روشن و باکیفیت
347
- FLY_WHITE_HEX = "#FFFFFF"
348
- FLY_BORDER_COLOR_HEX = "#E2E8F0"
349
- FLY_INPUT_BG_HEX_SIMPLE = "#F1F5F9"
350
- FLY_PANEL_BG_SIMPLE = "#F0F9FF"
351
-
352
- app_theme_outer = gr.themes.Base(font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"]).set(body_background_fill=FLY_LIGHT_BACKGROUND_HEX)
353
-
354
- custom_css = f"""
355
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
356
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
357
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
358
-
359
- :root {{
360
- --fly-primary: {FLY_PRIMARY_COLOR_HEX};
361
- --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
362
- --fly-accent: {FLY_ACCENT_COLOR_HEX};
363
- --fly-text-primary: {FLY_TEXT_COLOR_HEX};
364
- --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX};
365
- --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
366
- --fly-bg-white: {FLY_WHITE_HEX};
367
- --fly-border-color: {FLY_BORDER_COLOR_HEX};
368
- --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE};
369
- --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
370
- --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
371
- --font-english: 'Poppins', 'Inter', system-ui, sans-serif;
372
- --radius-xl: 1.25rem;
373
- --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.05), 0 8px 10px -6px rgba(0,0,0,0.05);
374
- --fly-primary-rgb: 79,70,229;
375
- --fly-accent-rgb: 217,119,6;
376
- }}
377
-
378
- body {{
379
- font-family: var(--font-global);
380
- direction: rtl;
381
- background-color: var(--fly-bg-light);
382
- color: var(--fly-text-primary);
383
- line-height: 1.8;
384
- -webkit-font-smoothing: antialiased;
385
- font-size: 16px;
386
- }}
387
-
388
- .gradio-container {{
389
- max-width: 100% !important;
390
- width: 100% !important;
391
- min-height: 100vh;
392
- margin: 0 auto !important;
393
- padding: 0 !important;
394
- background: linear-gradient(135deg, #EEF2F6 0%, #E3E9F1 100%);
395
- display: flex;
396
- flex-direction: column;
397
- }}
398
-
399
- .app-title-card {{
400
- text-align: center;
401
- padding: 3rem 1rem;
402
- margin: 0;
403
- background: linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%);
404
- color: var(--fly-bg-white);
405
- border-bottom-left-radius: var(--radius-xl);
406
- border-bottom-right-radius: var(--radius-xl);
407
- box-shadow: var(--shadow-xl);
408
- position: relative;
409
- overflow: hidden;
410
- }}
411
-
412
- .app-title-card::before {{
413
- content: '';
414
- position: absolute;
415
- top: -40px;
416
- right: -40px;
417
- width: 160px;
418
- height: 160px;
419
- background: rgba(255,255,255,0.1);
420
- border-radius: 9999px;
421
- opacity: 0.6;
422
- transform: rotate(45deg);
423
- }}
424
-
425
- .app-title-card h1 {{
426
- font-size: 2.5em !important;
427
- font-weight: 800 !important;
428
- margin: 0 0 0.5rem 0;
429
- font-family: var(--font-global);
430
- letter-spacing: -0.5px;
431
- text-shadow: 0 2px 4px rgba(0,0,0,0.15);
432
- }}
433
-
434
- .app-title-card p {{
435
- font-size: 1.1em !important;
436
- margin-top: 0.25rem;
437
- font-weight: 400;
438
- color: rgba(255, 255, 255, 0.9) !important;
439
- }}
440
-
441
- .app-footer-fly {{
442
- text-align: center;
443
- font-size: 0.9em;
444
- color: var(--fly-text-secondary);
445
- margin-top: auto;
446
- padding: 1.5rem 0;
447
- background-color: rgba(255,255,255,0.4);
448
- backdrop-filter: blur(8px);
449
- border-top: 1px solid var(--fly-border-color);
450
- }}
451
-
452
- footer, .gradio-footer, .flagging-container, .flex.row.gap-2.absolute.bottom-2.right-2 {{
453
- display: none !important;
454
- visibility: hidden !important;
455
- }}
456
-
457
- .main-content-area {{
458
- flex-grow: 1;
459
- padding: 1rem;
460
- width: 100%;
461
- margin: 0 auto;
462
- box-sizing: border-box;
463
- }}
464
-
465
- .content-panel-simple {{
466
- background-color: var(--fly-bg-white);
467
- padding: 1.5rem;
468
- border-radius: var(--radius-xl);
469
- box-shadow: var(--shadow-xl);
470
- margin-top: -2.5rem;
471
- position: relative;
472
- z-index: 10;
473
- margin-bottom: 2rem;
474
- width: 100%;
475
- box-sizing: border-box;
476
- border: 1px solid rgba(255,255,255,0.8);
477
- }}
478
-
479
- .content-panel-simple .gr-button.lg.primary {{
480
- background: linear-gradient(135deg, var(--fly-primary) 0%, var(--fly-secondary) 100%) !important;
481
- margin-top: 1.5rem !important;
482
- padding: 14px 24px !important;
483
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
484
- color: white !important;
485
- font-weight: 600 !important;
486
- border-radius: 12px !important;
487
- border: none !important;
488
- box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3) !important;
489
- width: 100% !important;
490
- }}
491
-
492
- .content-panel-simple .gr-button.lg.primary:hover {{
493
- transform: translateY(-2px) !important;
494
- box-shadow: 0 6px 16px rgba(79, 70, 229, 0.45) !important;
495
- filter: brightness(1.05);
496
- }}
497
-
498
- .content-panel-simple .gr-input > label + div > textarea,
499
- .content-panel-simple .gr-dropdown > label + div > div > input,
500
- .content-panel-simple .gr-textbox > label + div > textarea,
501
- .content-panel-simple .gr-dropdown select {{
502
- border-radius: 10px !important;
503
- border: 1.5px solid var(--fly-border-color) !important;
504
- background-color: var(--fly-input-bg-simple) !important;
505
- padding: 12px 14px !important;
506
- transition: all 0.2s ease;
507
- }}
508
-
509
- .content-panel-simple .gr-input > label + div > textarea:focus,
510
- .content-panel-simple .gr-dropdown > label + div > div > input:focus,
511
- .content-panel-simple .gr-textbox > label + div > textarea:focus,
512
- .content-panel-simple .gr-dropdown select:focus {{
513
- border-color: var(--fly-primary) !important;
514
- box-shadow: 0 0 0 3px rgba(var(--fly-primary-rgb), 0.15) !important;
515
- background-color: var(--fly-bg-white) !important;
516
- }}
517
-
518
- .content-panel-simple .gr-textbox[label*="ترجمه شده"] > label + div > textarea {{
519
- background-color: var(--fly-panel-bg-simple) !important;
520
- border-color: #BAE6FD !important;
521
- font-size: 1.05em !important;
522
- font-weight: 500;
523
- }}
524
-
525
- .content-panel-simple div[label*="تنظیمات پیشرفته"] .gr-panel {{
526
- border-radius: 10px !important;
527
- border: 1px solid var(--fly-border-color) !important;
528
- background-color: var(--fly-input-bg-simple) !important;
529
- }}
530
-
531
- @media (min-width: 768px) {{
532
- .main-content-area {{ max-width: 960px; }}
533
- .content-panel-simple {{ padding: 2.5rem; }}
534
- .content-panel-simple .main-content-row {{ display: flex !important; gap: 2rem !important; }}
535
- .content-panel-simple .gr-button.lg.primary {{ width: auto !important; align-self: flex-start; }}
536
- }}
537
- """
538
-
539
- with gr.Blocks(theme=app_theme_outer, css=custom_css, title="Alpha Universal Translator") as demo:
540
- gr.HTML(f"""
541
- <div class="app-title-card">
542
- <h1>🚀 مترجم هوشمند صوتی آلفا</h1>
543
- <p>ترجمه دقیق و تلفظ هوشمند تمامی زبان‌ها با هوش مصنوعی</p>
544
- </div>
545
- """)
546
-
547
- with gr.Column(elem_classes=["main-content-area"]):
548
- with gr.Group(elem_classes=["content-panel-simple"]):
549
- with gr.Row(elem_classes=["main-content-row"]):
550
- # ستون چپ - ورودی‌ها و تنظیمات
551
- with gr.Column(scale=3, min_width=320):
552
- input_text = gr.Textbox(lines=5, label="📝 متن مورد نظر برای ترجمه", placeholder="متن خود را به هر زبانی در این قسمت وارد یا جای‌گذاری کنید...")
553
-
554
- with gr.Row():
555
- source_lang = gr.Dropdown(choices=SOURCE_LANGUAGES, value="شناسایی خودکار", label="🌐 زبان مبدأ")
556
- target_lang = gr.Dropdown(choices=TARGET_LANGUAGES, value="انگلیسی", label="🎯 زبان مقصد")
557
-
558
- # مقداردهی گویندگان براساس زبان مقصد اولیه (انگلیسی)
559
- init_lang_key = "انگلیسی"
560
- init_voices = list(LANGUAGES_MAP[init_lang_key]["voices"].keys())
561
- voice_dropdown = gr.Dropdown(choices=init_voices, value=init_voices[0], label="🗣️ انتخاب گوینده و جنسیت صدا")
562
-
563
- with gr.Accordion("⚙️ تنظیمات تخصصی و گام صدا (سرعت، فرکانس و حجم)", open=False):
564
- with gr.Row():
565
- rate_slider = gr.Slider(-100, 100, 0, step=1, label="سرعت (%)", scale=1)
566
- volume_slider = gr.Slider(-100, 100, 0, step=1, label="حجم صدا (%)", scale=1)
567
- pitch_slider = gr.Slider(-50, 50, 0, step=1, label="گام صدا (Hz)")
568
-
569
- submit_button = gr.Button("🚀 ترجمه صوتی و تلفظ هوشمند", variant="primary", elem_classes=["lg"])
570
-
571
- # ستون راست - خروجی‌ها
572
- with gr.Column(scale=2, min_width=280):
573
- output_text_translated = gr.Textbox(label="📜 متن ترجمه شده", interactive=False, lines=7, placeholder="ترجمه نهایی در این بخش ظاهر خواهد شد...")
574
- output_audio = gr.Audio(type="filepath", label="🎧 پخش صوتی آنلاین", interactive=False, autoplay=True)
575
-
576
- # آپدیت پویا لیست صداها با تغییر زبان مقصد
577
- target_lang.change(
578
- fn=handle_target_lang_change,
579
- inputs=[target_lang],
580
- outputs=[voice_dropdown]
581
- )
582
-
583
- # دکمه اجرای اصلی
584
- submit_button.click(
585
- fn=translate_and_speak_multi_wrapper,
586
- inputs=[input_text, source_lang, target_lang, voice_dropdown, rate_slider, volume_slider, pitch_slider],
587
- outputs=[output_text_translated, output_audio]
588
- )
589
-
590
- # مثال‌های پیش‌فرض
591
- gr.HTML("<hr class='custom-hr' style='margin: 1.5rem 0;'>")
592
- gr.Examples(
593
- examples=[
594
- ["سلام، فردا هوا چطور است؟", "فارسی", "انگلیسی", "زن - جنی (آمریکا)", 0, 0, 0],
595
- ["Welcome to our advanced voice translation application.", "انگلیسی", "عربی", "مرد - شاکر (مصر)", 0, 0, 0],
596
- ["ببخشید، نزدیک‌ترین ایستگاه قطار کجاست؟", "شناسایی خودکار", "فرانسوی", "زن - الویز (فرانسه)", -5, 0, 0],
597
- ],
598
- inputs=[input_text, source_lang, target_lang, voice_dropdown, rate_slider, volume_slider, pitch_slider],
599
- outputs=[output_text_translated, output_audio],
600
- fn=translate_and_speak_multi_wrapper,
601
- cache_examples=os.getenv("GRADIO_CACHE_EXAMPLES", "False").lower() == "true",
602
- label="💡 مثال‌های ��ماده جهت تست سریع"
603
- )
604
 
605
- # --- بخش اختصاصی APIهای قدیمی جهت حفظ اتصال بدون قطعی ---
606
- # این بخش به صورت مخفی در برنامه تعریف می‌شود تا شناسه ورودی‌ها و خروجی‌های API برای کلاینت‌های قبلی کاملاً ثابت بماند.
607
- with gr.Row(visible=False):
608
- legacy_persian_text = gr.Textbox(label="legacy_persian_text", visible=False)
609
- legacy_voice_key = gr.Textbox(label="legacy_voice_key", visible=False)
610
- legacy_rate = gr.Slider(-100, 100, 0, label="legacy_rate", visible=False)
611
- legacy_volume = gr.Slider(-100, 100, 0, label="legacy_volume", visible=False)
612
- legacy_pitch = gr.Slider(-50, 50, 0, label="legacy_pitch", visible=False)
 
613
 
614
- legacy_output_text = gr.Textbox(label="legacy_output_text", visible=False)
615
- legacy_output_audio = gr.Audio(label="legacy_output_audio", visible=False)
616
 
617
- legacy_trigger_btn = gr.Button("legacy_trigger", visible=False)
 
618
 
619
- legacy_trigger_btn.click(
620
- fn=translate_and_speak_async_wrapper,
621
- inputs=[legacy_persian_text, legacy_voice_key, legacy_rate, legacy_volume, legacy_pitch],
622
- outputs=[legacy_output_text, legacy_output_audio],
623
- api_name="translate_and_speak" # تعیین نام مستقیم API برای ارجاع بدون خطا
624
- )
625
-
626
- gr.Markdown("<p class='app-footer-fly'>Alpha Language Learning © 2026</p>")
627
-
628
- if __name__ == "__main__":
629
- threading.Thread(target=auto_restart_service, daemon=True, name="AutoRestartThread").start()
630
- demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)), show_error=True)
 
 
 
 
 
 
 
 
 
1
  import os
 
2
  import time
3
+ import asyncio
4
+ import logging
5
  import threading
6
+ from typing import List, Optional, Any
7
+ from fastapi import FastAPI, Request
8
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel
11
+ import edge_tts
12
  from deep_translator import GoogleTranslator
13
+ import uuid
14
 
15
+ # --- پیکربندی اولیه ---
16
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
17
+ AUDIO_DIR = "audio_cache"
18
+ os.makedirs(AUDIO_DIR, exist_ok=True)
19
 
20
+ # پاکسازی فایلهای صوتی قدیمی برای جلوگیری از پر شدن هاست
21
+ def cleanup_old_audio():
22
+ while True:
23
+ try:
24
+ now = time.time()
25
+ for filename in os.listdir(AUDIO_DIR):
26
+ filepath = os.path.join(AUDIO_DIR, filename)
27
+ if os.stat(filepath).st_mtime < now - 3600: # حذف فایلهای قدیمی‌تر از ۱ ساعت
28
+ os.remove(filepath)
29
+ except Exception as e:
30
+ logging.error(f"Cleanup error: {e}")
31
+ time.sleep(1800)
32
 
33
+ threading.Thread(target=cleanup_old_audio, daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ # --- دیتابیس زبان‌ها و صداها ---
36
  LANGUAGES_MAP = {
37
+ "انگلیسی": {"code": "en", "voices": {'زن - جنی (آمریکا)': 'en-US-JennyNeural', 'مرد - گای (آمریکا)': 'en-US-GuyNeural', 'زن - آریا (آمریکا)': 'en-US-AriaNeural', 'مرد - کریستوفر (آمریکا)': 'en-US-ChristopherNeural', 'زن - لیبی (بریتانیا)': 'en-GB-LibbyNeural', 'مرد - رایان (بریتانیا)': 'en-GB-RyanNeural', 'زن - ناتاشا (استرالیا)': 'en-AU-NatashaNeural', 'مرد - لیام (کانادا)': 'en-CA-LiamNeural'}},
38
+ "فارسی": {"code": "fa", "voices": {'زن - دلارا': 'fa-IR-DilaraNeural', 'مرد - فرید': 'fa-IR-FaridNeural'}},
39
+ "عربی": {"code": "ar", "voices": {'زن - فاطمه (امارات)': 'ar-AE-FatimaNeural', 'مرد - حمدان (امارات)': 'ar-AE-HamdanNeural', 'زن - سلما (مصر)': 'ar-EG-SalmaNeural', 'مرد - شاکر (مصر)': 'ar-EG-ShakirNeural', 'مرد - حامد (عربستان)': 'ar-SA-HamedNeural'}},
40
+ "ترکی": {"code": "tr", "voices": {'زن - امل': 'tr-TR-EmelNeural', 'مرد - احمد': 'tr-TR-AhmetNeural'}},
41
+ "فرانسوی": {"code": "fr", "voices": {'زن - الویز (فرانسه)': 'fr-FR-EloiseNeural', 'مرد - هنری (فرانسه)': 'fr-FR-HenriNeural', 'زن - سیلویا (کانادا)': 'fr-CA-SylvieNeural'}},
42
+ "آلمانی": {"code": "de", "voices": {'زن - کاترین': 'de-DE-KatjaNeural', 'مرد - کنراد': 'de-DE-ConradNeural'}},
43
+ "اسپانیایی": {"code": "es", "voices": {'زن - الویرا (اسپانیا)': 'es-ES-ElviraNeural', 'مرد - آلوارو (اسپانیا)': 'es-ES-AlvaroNeural', 'زن - دالیا کزیک)': 'es-MX-DaliaNeural'}},
44
+ "روسی": {"code": "ru", "voices": {'زن - سوتلانا': 'ru-RU-SvetlanaNeural', 'مرد - دیمیتری': 'ru-RU-DmitryNeural'}},
45
+ "چینی": {"code": "zh-CN", "voices": {'زن - شیائوشیا': 'zh-CN-XiaoxiaNeural', 'مرد - یونشی': 'zh-CN-YunxiNeural'}},
46
+ "ژاپنی": {"code": "ja", "voices": {'زن - نانامی': 'ja-JP-NanamiNeural', 'مرد - کیتا': 'ja-JP-KeitaNeural'}},
47
+ "ایتالیایی": {"code": "it", "voices": {'زن - السا': 'it-IT-ElsaNeural', 'مرد - دیگو': 'it-IT-DiegoNeural'}},
48
+ "کره‌ای": {"code": "ko", "voices": {'زن - سونوهی': 'ko-KR-SunHiNeural', 'مرد - اینجون': 'ko-KR-InJoonNeural'}},
49
+ "هندی": {"code": "hi", "voices": {'زن - مادور': 'hi-IN-MadhurNeural', 'مرد - سوآرا': 'hi-IN-SwaraNeural'}},
50
+ "هلندی": {"code": "nl", "voices": {'زن - کوئلت': 'nl-NL-ColetteNeural', 'مرد - مارتین': 'nl-NL-MaartenNeural'}},
51
+ "سوئدی": {"code": "sv", "voices": {'زن - سوفی': 'sv-SE-SofieNeural', 'مرد - ماتیاس': 'sv-SE-MattiasNeural'}},
52
+ "لهستانی": {"code": "pl", "voices": {'زن - زوفیا': 'pl-PL-ZofiaNeural', 'مرد - مارک': 'pl-PL-MarekNeural'}},
53
+ "پرتغالی": {"code": "pt", "voices": {'زن - فرانسیسکا (برزیل)': 'pt-BR-FranciscaNeural', 'مرد - آنتونیو (برزیل)': 'pt-BR-AntonioNeural'}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  }
55
 
56
+ # --- دیتابیس قدیمی برای پشتیبانی API دوستان شما ---
57
+ LEGACY_VOICES = {
58
+ 'انگلیسی (آمریکا) - جنی (زن)': 'en-US-JennyNeural', 'انگلیسی (آمریکا) - گای (مرد)': 'en-US-GuyNeural',
59
+ 'انگلیسی (آمریکا) - آنا (زن، صدای کودک)': 'en-US-AnaNeural', 'انگلیسی (آمریکا) - آریا (زن)': 'en-US-AriaNeural',
60
+ 'انگلیسی (بریتانیا) - لیبی (زن)': 'en-GB-LibbyNeural', 'انگلیسی (بریتانیا) - رایان (مرد)': 'en-GB-RyanNeural',
61
+ }
 
 
 
62
 
63
+ # --- راه‌اندازی سرور ---
64
+ app = FastAPI(title="Alpha Translator API")
65
+ app.mount("/audio_cache", StaticFiles(directory=AUDIO_DIR), name="audio_cache")
 
 
 
 
 
 
66
 
67
+ # --- توابع اصلی پردازش ---
68
+ async def translate_text(text: str, source: str, target: str) -> str:
69
+ def _do_translate():
70
+ return GoogleTranslator(source=source, target=target).translate(text)
71
  try:
72
+ return await asyncio.to_thread(_do_translate)
 
 
 
 
73
  except Exception as e:
74
+ logging.error(f"Translation Error: {e}")
75
+ return f"خطا در ترجمه: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ async def generate_audio(text: str, voice_id: str, rate: int, volume: int, pitch: int) -> str:
78
+ filename = f"{uuid.uuid4().hex}.mp3"
79
+ filepath = os.path.join(AUDIO_DIR, filename)
80
+ rate_str, volume_str, pitch_str = f"{int(rate):+g}%", f"{int(volume):+g}%", f"{int(pitch):+g}Hz"
 
81
  try:
82
+ communicate = edge_tts.Communicate(text, voice_id, rate=rate_str, volume=volume_str, pitch=pitch_str)
83
+ await communicate.save(filepath)
84
+ return f"/audio_cache/{filename}"
 
 
 
 
85
  except Exception as e:
86
+ logging.error(f"TTS Error: {e}")
87
+ return ""
88
+
89
+ # --- مدل‌های داده (برای دریافت اطلاعات از فرانت‌اند) ---
90
+ class TranslateRequest(BaseModel):
91
+ text: str
92
+ source_lang: str
93
+ target_lang: str
94
+ voice_key: str
95
+ rate: int = 0
96
+ volume: int = 0
97
+ pitch: int = 0
98
+
99
+ class GradioLegacyRequest(BaseModel):
100
+ data: List[Any] # [text, voice, rate, volume, pitch]
101
+
102
+ # --- Endpoints API وب‌سایت ---
103
+ @app.get("/")
104
+ async def serve_index():
105
+ return FileResponse("index.html")
106
+
107
+ @app.get("/api/config")
108
+ async def get_config():
109
+ return {"languages": LANGUAGES_MAP}
110
+
111
+ @app.post("/api/translate_and_speak")
112
+ async def api_translate_speak(req: TranslateRequest):
113
+ src_code = "auto" if req.source_lang == "شناسایی خودکار" else LANGUAGES_MAP.get(req.source_lang, {}).get("code", "auto")
114
+ target_lang_data = LANGUAGES_MAP.get(req.target_lang, {})
115
+ tgt_code = target_lang_data.get("code", "en")
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ # پیدا کردن آیدی دقیق صدا
118
+ voice_id = target_lang_data.get("voices", {}).get(req.voice_key)
119
  if not voice_id:
120
+ voices = list(target_lang_data.get("voices", {}).values())
121
+ voice_id = voices[0] if voices else "en-US-JennyNeural"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ # 1. ترجمه
124
+ translated_text = await translate_text(req.text, src_code, tgt_code)
125
+ if "خطا در ترجمه" in translated_text:
126
+ return {"success": False, "error": translated_text}
127
 
128
+ # 2. تولید صدا
129
+ audio_url = await generate_audio(translated_text, voice_id, req.rate, req.volume, req.pitch)
 
 
 
130
 
131
+ return {
132
+ "success": True,
133
+ "translated_text": translated_text,
134
+ "audio_url": audio_url
135
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ # --- شبیه‌ساز API گاردین برای ربات‌ها/اسکریپت‌های دوستان شما ---
138
+ @app.post("/run/translate_and_speak")
139
+ @app.post("/api/translate_and_speak_legacy")
140
+ async def legacy_api(req: GradioLegacyRequest, request: Request):
141
+ """
142
+ این بخش دقیقا ساختار Gradio را شبیه سازی می‌کند تا هیچ اسکریپتی از کار نیفتد.
143
+ """
144
+ try:
145
+ text, voice_key, rate, volume, pitch = req.data[0], req.data[1], req.data[2], req.data[3], req.data[4]
146
 
147
+ # ترجمه به انگلیسی (رفتار قبلی سیستم)
148
+ translated_text = await translate_text(text, "fa", "en")
149
 
150
+ # پیدا کردن صدای انگلیسی قدیمی
151
+ voice_id = LEGACY_VOICES.get(voice_key, "en-US-JennyNeural")
152
 
153
+ audio_url = await generate_audio(translated_text, voice_id, rate, volume, pitch)
154
+
155
+ # ساختن آدرس کامل فایل صوتی برای کلاینت‌ها
156
+ base_url = str(request.base_url).rstrip("/")
157
+ full_audio_url = f"{base_url}{audio_url}" if audio_url else None
158
+
159
+ # خروجی دقیقاً با ساختار پاسخ Gradio
160
+ return JSONResponse(content={
161
+ "data": [
162
+ translated_text,
163
+ {"name": full_audio_url, "data": full_audio_url, "is_file": True} if full_audio_url else None
164
+ ]
165
+ })
166
+ except Exception as e:
167
+ return JSONResponse(content={"error": str(e)}, status_code=500)