Hamed744 commited on
Commit
db88be5
·
verified ·
1 Parent(s): 86c8c2e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -253
app.py CHANGED
@@ -1,279 +1,351 @@
1
- # ... (بخش‌های قبلی کد بدون تغییر) ...
 
 
 
 
 
 
 
 
 
2
 
3
- # --- START: CSS و متغیرهای رنگی از کد اول (Alpha Translator) ---
4
- FLY_PRIMARY_COLOR_HEX = "#4F46E5"
5
- FLY_SECONDARY_COLOR_HEX = "#10B981"
6
- FLY_ACCENT_COLOR_HEX = "#D97706" # این رنگ برای دکمه اصلی در کد اول استفاده شده بود
7
- FLY_TEXT_COLOR_HEX = "#1F2937"
8
- FLY_SUBTLE_TEXT_HEX = "#6B7280"
9
- FLY_LIGHT_BACKGROUND_HEX = "#F9FAFB"
10
- FLY_WHITE_HEX = "#FFFFFF"
11
- FLY_BORDER_COLOR_HEX = "#D1D5DB"
12
- FLY_INPUT_BG_HEX_SIMPLE = "#F3F4F6"
13
- FLY_PANEL_BG_SIMPLE = "#E0F2FE" # برای پس زمینه بخش ترجمه شده در کد اول
14
 
15
- app_theme_outer = gr.themes.Base(
16
- font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
17
- ).set(
18
- body_background_fill=FLY_LIGHT_BACKGROUND_HEX,
19
- )
 
 
 
 
 
 
20
 
21
- custom_css = f"""
22
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
23
- @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap');
24
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
25
- :root {{
26
- --fly-primary: {FLY_PRIMARY_COLOR_HEX}; --fly-secondary: {FLY_SECONDARY_COLOR_HEX};
27
- --fly-accent: {FLY_ACCENT_COLOR_HEX}; --fly-text-primary: {FLY_TEXT_COLOR_HEX};
28
- --fly-text-secondary: {FLY_SUBTLE_TEXT_HEX}; --fly-bg-light: {FLY_LIGHT_BACKGROUND_HEX};
29
- --fly-bg-white: {FLY_WHITE_HEX}; --fly-border-color: {FLY_BORDER_COLOR_HEX};
30
- --fly-input-bg-simple: {FLY_INPUT_BG_HEX_SIMPLE}; --fly-panel-bg-simple: {FLY_PANEL_BG_SIMPLE};
31
- --font-global: 'Vazirmatn', 'Inter', 'Poppins', system-ui, sans-serif;
32
- --font-english: 'Poppins', 'Inter', system-ui, sans-serif; /* برای متن انگلیسی در صورت نیاز */
33
- --radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem; --radius-xl: 1rem; --radius-full: 9999px;
34
- --shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05); --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1);
35
- --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1);
36
- --shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1);
37
- --fly-primary-rgb: 79,70,229; --fly-accent-rgb: 217,119,6;
38
- }}
39
- body {{font-family:var(--font-global);direction:rtl;background-color:var(--fly-bg-light);color:var(--fly-text-primary);line-height:1.7;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;}}
40
- .gradio-container {{max-width:100% !important;width:100% !important;min-height:100vh;margin:0 auto !important;padding:0 !important;border-radius:0 !important;box-shadow:none !important;background:linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%);display:flex;flex-direction:column;}}
41
- .app-title-card {{text-align:center;padding:2.5rem 1rem;margin:0;background:linear-gradient(135deg,var(--fly-primary) 0%,var(--fly-secondary) 100%);color:var(--fly-bg-white);border-bottom-left-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl);box-shadow:var(--shadow-lg);position:relative;overflow:hidden;}}
42
- .app-title-card::before {{content:'';position:absolute;top:-50px;right:-50px;width:150px;height:150px;background:rgba(255,255,255,0.1);border-radius:var(--radius-full);opacity:0.5;transform:rotate(45deg);}}
43
- .app-title-card h1 {{font-size:2.25em !important;font-weight:800 !important;margin:0 0 0.5rem 0;font-family:var(--font-english);letter-spacing:-0.5px;text-shadow:0 2px 4px rgba(0,0,0,0.1);}} /* فونت انگلیسی برای عنوان اصلی */
44
- .app-title-card p {{font-size:1em !important;margin-top:0.25rem;font-weight:400;color:rgba(255,255,255,0.85) !important;}}
45
- .app-footer-fly {{text-align:center;font-size:0.85em;color:var(--fly-text-secondary);margin-top:2.5rem;padding:1rem 0;background-color:rgba(255,255,255,0.3);backdrop-filter:blur(5px);border-top:1px solid var(--fly-border-color);}}
46
- footer,.gradio-footer,.flagging-container,.flex.row.gap-2.absolute.bottom-2.right-2.gr-compact.gr-box.gr-text-gray-500,div[data-testid="flag"],button[title="Flag"],button[aria-label="Flag"],.footer-utils {{display:none !important;visibility:hidden !important;}}
47
- .main-content-area {{flex-grow:1;padding:0.75rem;width:100%;margin:0 auto;box-sizing:border-box;}}
48
- .content-panel-simple {{background-color:var(--fly-bg-white);padding:1rem;border-radius:var(--radius-xl);box-shadow:var(--shadow-xl);margin-top:-2rem;position:relative;z-index:10;margin-bottom:2rem;width:100%;box-sizing:border-box;}}
49
 
50
- /* استایل دکمه اصلی با استفاده از رنگ accent کد اول */
51
- .content-panel-simple .gr-button.lg.primary, .content-panel-simple button[variant="primary"].lg {{
52
- background:var(--fly-accent) !important;
53
- margin-top:1rem !important;padding:12px 20px !important;
54
- transition:all 0.25s ease-in-out !important;color:white !important;
55
- font-weight:600 !important;border-radius:10px !important;
56
- border:none !important;box-shadow:0 3px 8px -1px rgba(var(--fly-accent-rgb),0.3) !important;
57
- width:100% !important;font-size:1em !important;
58
- display:flex;align-items:center;justify-content:center;
59
- }}
60
- .content-panel-simple .gr-button.lg.primary:hover, .content-panel-simple button[variant="primary"].lg:hover {{
61
- background:#B45309 !important; /* رنگ تیره‌تر fly-accent برای هاور */
62
- transform:translateY(-1px) !important;
63
- box-shadow:0 5px 10px -1px rgba(var(--fly-accent-rgb),0.4) !important;
64
- }}
65
 
66
- .content-panel-simple .gr-input > label + div > textarea,
67
- .content-panel-simple .gr-dropdown > label + div > div > input,
68
- .content-panel-simple .gr-dropdown > label + div > div > select,
69
- .content-panel-simple .gr-textbox > label + div > textarea,
70
- .content-panel-simple .gr-file > label + div /* اضافه شده برای فایل */
71
- {{
72
- border-radius:8px !important;border:1.5px solid var(--fly-border-color) !important;
73
- font-size:0.95em !important;background-color:var(--fly-input-bg-simple) !important;
74
- padding:10px 12px !important;color:var(--fly-text-primary) !important;
75
- }}
76
- .content-panel-simple .gr-file > label + div {{ text-align:center; border-style: dashed !important; }} /* استایل برای درگ و دراپ فایل */
77
 
78
- .content-panel-simple .gr-input > label + div > textarea:focus,
79
- .content-panel-simple .gr-dropdown > label + div > div > input:focus,
80
- .content-panel-simple .gr-dropdown > label + div > div > select:focus,
81
- .content-panel-simple .gr-textbox > label + div > textarea:focus,
82
- .content-panel-simple .gr-file > label + div:focus-within /* اضافه شده برای فایل */
83
- {{
84
- border-color:var(--fly-primary) !important;
85
- box-shadow:0 0 0 3px rgba(var(--fly-primary-rgb),0.12) !important;
86
- background-color:var(--fly-bg-white) !important;
87
- }}
88
- .content-panel-simple .gr-dropdown select {{font-family:var(--font-global) !important;width:100%;cursor:pointer;}}
89
 
90
- /* استایل برای تکست‌باکس خروجی (پیام وضعیت) */
91
- .content-panel-simple .gr-textbox[label*="وضعیت"] > label + div > textarea,
92
- .content-panel-simple .gr-textbox[elem_id="status_text_output_tts"] > label + div > textarea {{
93
- background-color:var(--fly-panel-bg-simple) !important;
94
- border-color:#A5D5FE !important;min-height:60px; /* ارتفاع کمتر برای پیام وضعیت */
95
- font-family:var(--font-global); /* فونت فارسی برای پیام‌ها */
96
- font-size:0.9em !important;line-height:1.5;padding:8px 10px !important;
97
- }}
 
 
 
 
 
 
 
 
 
98
 
99
- .content-panel-simple .gr-panel,
100
- .content-panel-simple div[label*="تنظیمات پیشرفته"] > .gr-accordion > .gr-panel /* اگر آکاردئون اضافه شود */
101
- {{
102
- border-radius:8px !important;border:1px solid var(--fly-border-color) !important;
103
- background-color:var(--fly-input-bg-simple) !important;
104
- padding:0.8rem 1rem !important;margin-top:0.6rem;box-shadow:none;
105
- }}
106
- .content-panel-simple label > span.label-text {{font-weight:500 !important;color:#4B5563 !important;font-size:0.88em !important;margin-bottom:6px !important;display:inline-block;}}
107
- .content-panel-simple .gr-slider label span {{font-size:0.82em !important;color:var(--fly-text-secondary);}}
 
 
108
 
109
- .content-panel-simple div[label*="نمونه"], .content-panel-simple .gr-examples[label*="نمونه"] {{margin-top:1.5rem;}}
110
- .content-panel-simple div[label*="نمونه"] .gr-button.gr-button-tool,
111
- .content-panel-simple div[label*="نمونه"] .gr-sample-button,
112
- .content-panel-simple .gr-examples[label*="نمونه"] .gr-button.gr-button-tool,
113
- .content-panel-simple .gr-examples[label*="نمونه"] .gr-sample-button
114
- {{
115
- background-color:#E0E7FF !important;color:var(--fly-primary) !important;
116
- border-radius:6px !important;font-size:0.78em !important;padding:4px 8px !important;
117
- }}
118
- .content-panel-simple .custom-hr {{height:1px;background-color:var(--fly-border-color);margin:1.5rem 0;border:none;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- /* استایل برای پیام هشدار API Key مشابه کد اول */
121
- .api-warning-message {{
122
- background-color:#FFFBEB !important;color:#92400E !important;
123
- padding:10px 12px !important;border-radius:8px !important;
124
- border:1px solid #FDE68A !important;text-align:center !important;
125
- margin:0 0.2rem 1rem 0.2rem !important;font-size:0.85em !important;
126
- }}
 
127
 
128
- /* استایل برای پلیر صوتی */
129
- .content-panel-simple #output_audio_player_tts audio {{ width: 100%; border-radius: var(--radius-input, 8px); margin-top:0.8rem; }}
130
- .temp_description_class_tts {{ font-size: 0.85em; color: var(--fly-subtle-text, #777); margin-top: -0.4rem; margin-bottom: 1rem; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
 
133
- @media (min-width:640px) {{
134
- .main-content-area {{padding:1.5rem;max-width:700px;}}
135
- .content-panel-simple {{padding:1.5rem;}}
136
- .app-title-card h1 {{font-size:2.5em !important;}}
137
- .app-title-card p {{font-size:1.05em !important;}}
138
- }}
139
- @media (min-width:768px) {{
140
- .main-content-area {{max-width:780px;}}
141
- .content-panel-simple {{padding:2rem;}}
142
- /* .content-panel-simple .main-content-row {{display:flex !important;flex-direction:row !important;gap:1.5rem !important;}} */ /* این بخش برای چیدمان دو ستونه بود که در این کد دوم نداریم */
143
- /* .content-panel-simple .main-content-row > .gr-column:nth-child(1) {{flex-basis:60%;}} */
144
- /* .content-panel-simple .main-content-row > .gr-column:nth-child(2) {{flex-basis:40%;}} */
145
- .content-panel-simple .gr-button.lg.primary, .content-panel-simple button[variant="primary"].lg {{width:auto !important;align-self:flex-start;}}
146
- .app-title-card h1 {{font-size:2.75em !important;}}
147
- .app-title-card p {{font-size:1.1em !important;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  """
150
- # --- END: CSS و متغیرهای رنگی ---
151
 
 
 
 
 
 
 
152
 
153
- with gr.Blocks(theme=app_theme_outer, css=custom_css, title="آلفا TTS") as demo:
154
- # هدر مشابه کد اول، با تغییر عنوان و متن
155
- gr.HTML(f"""
156
- <div class="app-title-card">
157
- <h1>🚀 Alpha TTS</h1>
158
- <p>جادوی تبدیل متن به صدا در دستان شما</p>
159
- </div>
160
- """)
161
 
162
- with gr.Column(elem_classes=["main-content-area"]):
163
- with gr.Group(elem_classes=["content-panel-simple"]):
164
- # نمایش هشدار اگر کلید API تنظیم نشده باشد (مشابه کد اول)
165
- if not GEMINI_API_KEY_TTS:
166
- missing_key_msg_tts = (
167
- "⚠️ هشدار: قابلیت تبدیل متن به گفتار غیرفعال است. "
168
- "کلید API جیمینای (GEMINI_API_KEY) "
169
- "در بخش Secrets این Space یافت نشد. "
170
- "لطفاً آن را تنظیم کنید."
171
- )
172
- gr.Markdown(f"<div class='api-warning-message'>{missing_key_msg_tts}</div>")
173
-
174
- use_file_input_cb = gr.Checkbox(
175
- label="📄 استفاده از فایل متنی (.txt) برای ورود متن",
176
- value=False,
177
- elem_id="use_file_cb_tts"
178
- )
179
- uploaded_file_input = gr.File(
180
- label="انتخاب فایل .txt",
181
- file_types=['.txt'],
182
- visible=False,
183
- elem_id="file_uploader_tts"
184
- )
185
- text_to_speak_tb = gr.Textbox(
186
- label="📝 متن فارسی برای تبدیل به گفتار",
187
- placeholder="مثال: سلام، امروز هوا بسیار دلپذیر است.",
188
- lines=5,
189
- value="",
190
- visible=True,
191
- elem_id="text_input_tts"
192
- )
193
-
194
- def toggle_input_method(use_file):
195
- return gr.update(visible=use_file), gr.update(visible=not use_file)
196
 
197
- use_file_input_cb.change(
198
- fn=toggle_input_method,
199
- inputs=use_file_input_cb,
200
- outputs=[uploaded_file_input, text_to_speak_tb]
201
- )
 
 
 
 
 
 
 
 
 
202
 
203
- speech_prompt_tb = gr.Textbox(
204
- label="🗣️ سبک گفتار (اختیاری)",
205
- placeholder="مثال: با لحنی شاد و پرانرژی صحبت کن.",
206
- value="با لحنی دوستانه و رسا صحبت کن.",
207
- lines=2, elem_id="speech_prompt_tts"
208
- )
209
- speaker_voice_dd = gr.Dropdown(
210
- SPEAKER_VOICES,
211
- label="🎤 انتخاب گوینده",
212
- value="Charon",
213
- elem_id="speaker_voice_tts"
214
- )
215
- temperature_slider = gr.Slider(
216
- minimum=0.1, maximum=1.5, step=0.05, value=0.9,
217
- label="🌡️ میزان خلاقیت صدا (Temperature)",
218
- elem_id="temperature_slider_tts"
219
- )
220
- gr.Markdown("<p class='temp_description_class_tts'>مقادیر بالاتر = تنوع بیشتر در صدا، مقادیر پایین‌تر = صدای یکنواخت‌تر.</p>")
221
 
222
- generate_button = gr.Button(
223
- "🚀 تولید و پخش صدا",
224
- variant="primary",
225
- elem_classes=["lg"]
226
- )
227
-
228
- status_text_output = gr.Textbox(
229
- label="📜 وضعیت پردازش و پیام‌ها",
230
- interactive=False,
231
- lines=2,
232
- placeholder="پیام‌های وضعیت یا خطا در اینجا نمایش داده می‌شوند...",
233
- elem_id="status_text_output_tts"
234
- )
235
- output_audio = gr.Audio(
236
- label="🎧 فایل صوتی تولید شده",
237
- type="filepath",
238
- interactive=False,
239
- autoplay=True,
240
- elem_id="output_audio_player_tts"
241
- )
242
 
243
- generate_button.click(
244
- fn=gradio_tts_interface,
245
- inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
246
- outputs=[output_audio, status_text_output]
247
- )
248
-
249
- example_list_tts = [
250
- [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
251
- [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
252
- [False, None, "به دنیای شگفت‌انگیز تبدیل متن به گفتار خوش آمدید!", "با هیجان و انرژی بالا.", "Achird", 1.0],
253
- ]
254
-
255
- if example_list_tts:
256
- gr.Examples(
257
- examples=example_list_tts,
258
- inputs=[use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider],
259
- outputs=[output_audio, status_text_output],
260
- fn=gradio_tts_interface,
261
- cache_examples=False, # <<<--- تغییر اصلی اینجاست
262
- label="💡 نمونه‌های کاربردی"
263
- )
264
- else:
265
- gr.Markdown("<p style='text-align:center; color:var(--fly-text-secondary); margin-top:1rem;'>نمونه‌ای برای نمایش موجود نیست.</p>")
266
 
267
- gr.Markdown("<p class='app-footer-fly'>Alpha Language Learning © 2024</p>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
 
270
  if __name__ == "__main__":
271
- if not GEMINI_API_KEY_TTS:
272
- logging.warning("هشدار: کلید API جیمینای (GEMINI_API_KEY) برای TTS یافت نشد. برنامه اجرا می‌شود اما تولید صدا کار نخواهد کرد.")
273
-
274
- demo.launch(
275
- server_name="0.0.0.0",
276
- server_port=int(os.getenv("PORT", 7860)),
277
- debug=os.environ.get("GRADIO_DEBUG", "False").lower() == "true",
278
- show_error=True
279
- )
 
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
11
 
12
+ try:
13
+ from pydub import AudioSegment
14
+ PYDUB_AVAILABLE = True
15
+ except ImportError:
16
+ PYDUB_AVAILABLE = False
 
 
 
 
 
 
17
 
18
+ SPEAKER_VOICES = [
19
+ "Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager",
20
+ "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux",
21
+ "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib",
22
+ "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus",
23
+ "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"
24
+ ]
25
+ FIXED_MODEL_NAME = "gemini-2.5-flash-preview-tts"
26
+ DEFAULT_MAX_CHUNK_SIZE = 3800
27
+ DEFAULT_SLEEP_BETWEEN_REQUESTS = 8
28
+ DEFAULT_OUTPUT_FILENAME_BASE = "alpha_tts_audio" # نام فایل خروجی خودکار
29
 
30
+ def _log(message, log_list):
31
+ log_list.append(message) # برای دیباگ داخلی
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ def save_binary_file(file_name, data, log_list):
34
+ try:
35
+ with open(file_name, "wb") as f: f.write(data)
36
+ _log(f"✅ فایل ذخیره شد: {file_name}", log_list)
37
+ return file_name
38
+ except Exception as e:
39
+ _log(f"❌ خطا در ذخیره فایل {file_name}: {e}", log_list)
40
+ return None
 
 
 
 
 
 
 
41
 
42
+ def convert_to_wav(audio_data: bytes, mime_type: str) -> bytes:
43
+ parameters = parse_audio_mime_type(mime_type)
44
+ bits_per_sample, rate = parameters["bits_per_sample"], parameters["rate"]
45
+ num_channels, data_size = 1, len(audio_data)
46
+ bytes_per_sample, block_align = bits_per_sample // 8, num_channels * (bits_per_sample // 8)
47
+ byte_rate, chunk_size = rate * block_align, 36 + data_size
48
+ 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)
49
+ return header + audio_data
 
 
 
50
 
51
+ def parse_audio_mime_type(mime_type: str) -> dict[str, int]:
52
+ bits, rate = 16, 24000
53
+ for param in mime_type.split(";"):
54
+ param = param.strip()
55
+ if param.lower().startswith("rate="):
56
+ try: rate = int(param.split("=", 1)[1])
57
+ except: pass
58
+ elif param.startswith("audio/L"):
59
+ try: bits = int(param.split("L", 1)[1])
60
+ except: pass
61
+ return {"bits_per_sample": bits, "rate": rate}
62
 
63
+ def smart_text_split(text, max_size=3800, log_list=None):
64
+ if len(text) <= max_size: return [text]
65
+ chunks, current_chunk = [], ""
66
+ sentences = re.split(r'(?<=[.!?؟])\s+', text)
67
+ for sentence in sentences:
68
+ if len(current_chunk) + len(sentence) + 1 > max_size:
69
+ if current_chunk: chunks.append(current_chunk.strip())
70
+ current_chunk = sentence
71
+ while len(current_chunk) > max_size:
72
+ split_idx = next((i for i in range(max_size - 1, max_size // 2, -1) if current_chunk[i] in ['،', ',', ';', ':', ' ']), -1)
73
+ 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:])
74
+ chunks.append(part.strip())
75
+ else: current_chunk += (" " if current_chunk else "") + sentence
76
+ if current_chunk: chunks.append(current_chunk.strip())
77
+ final_chunks = [c for c in chunks if c]
78
+ if log_list: _log(f"📊 متن به {len(final_chunks)} قطعه تقسیم شد.", log_list)
79
+ return final_chunks
80
 
81
+ def merge_audio_files_func(file_paths, output_path, log_list):
82
+ if not PYDUB_AVAILABLE: _log(" pydub در دسترس نیست.", log_list); return False
83
+ try:
84
+ _log(f"🔗 ادغام {len(file_paths)} فایل صوتی...", log_list)
85
+ combined = AudioSegment.empty()
86
+ for i, fp in enumerate(file_paths):
87
+ if os.path.exists(fp): combined += AudioSegment.from_file(fp) + (AudioSegment.silent(duration=150) if i < len(file_paths) - 1 else AudioSegment.empty())
88
+ else: _log(f"⚠️ فایل پیدا نشد: {fp}", log_list)
89
+ combined.export(output_path, format="wav")
90
+ _log(f"✅ فایل ادغام شده: {output_path}", log_list); return True
91
+ except Exception as e: _log(f"❌ خطا در ادغام: {e}", log_list); return False
92
 
93
+ def core_generate_audio(text_input, prompt_input, selected_voice, temperature_val, log_list):
94
+ output_base_name = DEFAULT_OUTPUT_FILENAME_BASE # نام فایل ثابت
95
+ max_chunk, sleep_time = DEFAULT_MAX_CHUNK_SIZE, DEFAULT_SLEEP_BETWEEN_REQUESTS
96
+ _log("🚀 شروع فرآیند...", log_list)
97
+ api_key = os.environ.get("GEMINI_API_KEY")
98
+ if not api_key: _log("❌ کلید API تنظیم نشده.", log_list); return None
99
+ try: client = genai.Client(api_key=api_key)
100
+ except Exception as e: _log(f"❌ خطا در کلاینت: {e}", log_list); return None
101
+ if not text_input or not text_input.strip(): _log("❌ متن ورودی خالی.", log_list); return None
102
+ text_chunks = smart_text_split(text_input, max_chunk, log_list)
103
+ if not text_chunks: _log("❌ متن قابل پردازش نیست.", log_list); return None
104
+
105
+ generated_files = []
106
+ for i, chunk in enumerate(text_chunks):
107
+ _log(f"🔊 پردازش قطعه {i+1}/{len(text_chunks)}...", log_list)
108
+ final_text = f'"{prompt_input}"\n{chunk}' if prompt_input and prompt_input.strip() else chunk
109
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=final_text)])]
110
+ config = types.GenerateContentConfig(temperature=temperature_val, response_modalities=["audio"],
111
+ speech_config=types.SpeechConfig(voice_config=types.VoiceConfig(
112
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=selected_voice))))
113
+ fname_base = f"{output_base_name}_part{i+1:03d}" # نامگذاری قطعات موقت
114
+ try:
115
+ response = client.models.generate_content(model=FIXED_MODEL_NAME, contents=contents, config=config)
116
+ if response.candidates and response.candidates[0].content and response.candidates[0].content.parts and response.candidates[0].content.parts[0].inline_data:
117
+ inline_data = response.candidates[0].content.parts[0].inline_data
118
+ data_buffer = inline_data.data
119
+ ext = mimetypes.guess_extension(inline_data.mime_type) or ".wav"
120
+ if "audio/L" in inline_data.mime_type and ext == ".wav": data_buffer = convert_to_wav(data_buffer, inline_data.mime_type)
121
+ if not ext.startswith("."): ext = "." + ext
122
+ fpath = save_binary_file(f"{fname_base}{ext}", data_buffer, log_list)
123
+ if fpath: generated_files.append(fpath)
124
+ else: _log(f"⚠️ پاسخ API برای قطعه {i+1} بدون داده صوتی.", log_list)
125
+ except Exception as e: _log(f"❌ خطا در تولید قطعه {i+1}: {e}", log_list); continue
126
+ if i < len(text_chunks) - 1 and len(text_chunks) > 1: time.sleep(sleep_time)
127
 
128
+ if not generated_files: _log("❌ هیچ فایلی تولید نشد.", log_list); return None
129
+ _log(f"🎉 {len(generated_files)} فایل(های) صوتی تولید شد.", log_list)
130
+
131
+ final_audio_file = None
132
+ # نام فایل نهایی ادغام شده یا تکی
133
+ # استفاده از یک نام ثابت با افزودن timestamp برای جلوگیری از کش شدن توسط مرورگر یا CDN
134
+ # اما برای سادگی و چون پلیر Gradio معمولا با محتوا آپدیت می‌شود، فعلا timestamp نمی‌گذاریم.
135
+ final_output_path_base = f"{output_base_name}_final"
136
 
137
+ if len(generated_files) > 1:
138
+ if PYDUB_AVAILABLE:
139
+ merged_fn = f"{final_output_path_base}.wav" # همیشه WAV برای فایل نهایی
140
+ if os.path.exists(merged_fn): os.remove(merged_fn) # حذف فایل قبلی اگر وجود دارد
141
+ if merge_audio_files_func(generated_files, merged_fn, log_list):
142
+ final_audio_file = merged_fn
143
+ for fp in generated_files: # حذف فایل‌های جزئی
144
+ if os.path.abspath(fp) != os.path.abspath(merged_fn):
145
+ try: os.remove(fp)
146
+ except: pass
147
+ else: # اگر ادغام ناموفق بود، اولین قطعه با نام استاندارد ارائه می‌شود
148
+ if generated_files:
149
+ try:
150
+ os.rename(generated_files[0], f"{final_output_path_base}{os.path.splitext(generated_files[0])[1]}")
151
+ final_audio_file = f"{final_output_path_base}{os.path.splitext(generated_files[0])[1]}"
152
+ except Exception as e_rename:
153
+ _log(f"خطا در تغییر نام فایل اولین قطعه: {e_rename}", log_list)
154
+ final_audio_file = generated_files[0] # بازگشت به مسیر اصلی
155
+ # پاک کردن سایر فایل‌های جزئی حتی اگر ادغام ناموفق بود
156
+ if final_audio_file: # اگر فایلی برای ارائه داریم (ادغام شده یا اولین قطعه)
157
+ for fp_cleanup in generated_files:
158
+ if os.path.abspath(fp_cleanup) != os.path.abspath(final_audio_file):
159
+ try: os.remove(fp_cleanup)
160
+ except: pass
161
+ else:
162
+ _log("⚠️ pydub نیست. اولین قطعه ارائه می‌شود.", log_list)
163
+ if generated_files:
164
+ try:
165
+ os.rename(generated_files[0], f"{final_output_path_base}{os.path.splitext(generated_files[0])[1]}")
166
+ final_audio_file = f"{final_output_path_base}{os.path.splitext(generated_files[0])[1]}"
167
+ for i_gf in range(1, len(generated_files)): # پاک کردن بقیه فایل‌های جزئی
168
+ try: os.remove(generated_files[i_gf])
169
+ except: pass
170
+ except Exception as e_rename_single:
171
+ _log(f"خطا در تغییر نام فایل اولین قطعه (بدون pydub): {e_rename_single}", log_list)
172
+ final_audio_file = generated_files[0]
173
 
174
 
175
+ elif len(generated_files) == 1:
176
+ try:
177
+ # تغییر نام فایل تکی به نام استاندارد نهایی
178
+ target_ext = os.path.splitext(generated_files[0])[1]
179
+ final_single_fn = f"{final_output_path_base}{target_ext}"
180
+ if os.path.exists(final_single_fn): os.remove(final_single_fn)
181
+ os.rename(generated_files[0], final_single_fn)
182
+ final_audio_file = final_single_fn
183
+ except Exception as e_rename_single_final:
184
+ _log(f"خطا در تغییر نام فایل تکی نهایی: {e_rename_single_final}", log_list)
185
+ final_audio_file = generated_files[0] # بازگشت به مسیر اصلی اگر تغییر نام ناموفق بود
186
+
187
+ if final_audio_file and not os.path.exists(final_audio_file):
188
+ _log(f"⚠️ فایل نهایی '{final_audio_file}' وجود ندارد!", log_list)
189
+ return None
190
+ return final_audio_file
191
+
192
+ def gradio_tts_interface(use_file_input, uploaded_file, text_to_speak, speech_prompt, speaker_voice, temperature, progress=gr.Progress(track_tqdm=True)):
193
+ logs = []
194
+ actual_text = ""
195
+ if use_file_input:
196
+ if uploaded_file:
197
+ try:
198
+ with open(uploaded_file.name, 'r', encoding='utf-8') as f: actual_text = f.read().strip()
199
+ if not actual_text: return None
200
+ except Exception as e: _log(f"❌ خطا خواندن فایل: {e}", logs); return None
201
+ else: return None
202
+ else:
203
+ actual_text = text_to_speak
204
+ if not actual_text or not actual_text.strip(): return None
205
+
206
+ final_path = core_generate_audio(actual_text, speech_prompt, speaker_voice, temperature, logs)
207
+ # for log_entry in logs: print(log_entry) # For debugging in HF console
208
+ return final_path
209
+
210
+ # --- CSS با الهام دقیق‌تر از تصاویر نمونه ---
211
+ # متغیرهای رنگی اصلی از تصاویر شما (Alpha Translator)
212
+ APP_HEADER_GRADIENT_START = "#4A00E0" # بنفش تیره‌تر
213
+ APP_HEADER_GRADIENT_END = "#8E2DE2" # بنفش روشن‌تر
214
+ # یا گرادیانت آبی-سبز تصویر:
215
+ APP_HEADER_GRADIENT_START_IMG = "#2980b9" # آبی
216
+ APP_HEADER_GRADIENT_END_IMG = "#2ecc71" # سبز
217
+
218
+ PANEL_BACKGROUND = "#FFFFFF"
219
+ TEXT_INPUT_BG = "#F7F7F7" # یا #FFFFFF اگر در تصویر سفید است
220
+ BUTTON_BG_IMG = "#2979FF" # آبی دکمه در تصویر
221
+ MAIN_BACKGROUND_IMG = "linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%)" # پس زمینه کلی تصویر (مایل به بنفش و آبی روشن)
222
+
223
+ custom_css_inspired_by_image = f"""
224
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
225
+ :root {{
226
+ --app-font: 'Vazirmatn', sans-serif;
227
+ --app-header-grad-start: {APP_HEADER_GRADIENT_START_IMG};
228
+ --app-header-grad-end: {APP_HEADER_GRADIENT_END_IMG};
229
+ --app-panel-bg: {PANEL_BACKGROUND};
230
+ --app-input-bg: {TEXT_INPUT_BG};
231
+ --app-button-bg: {BUTTON_BG_IMG};
232
+ --app-main-bg: {MAIN_BACKGROUND_IMG};
233
+ --app-text-primary: #333;
234
+ --app-text-secondary: #555;
235
+ --app-border-color: #E0E0E0;
236
+ --radius-card: 20px; /* گردی بیشتر برای کارت اصلی */
237
+ --radius-input: 8px;
238
+ --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.1);
239
+ --shadow-button: 0 4px 10px -2px rgba(41,121,255,0.5);
240
  }}
241
+ body, .gradio-container {{ font-family: var(--app-font); direction: rtl; background: var(--app-main-bg); color: var(--app-text-primary); font-size: 16px; line-height: 1.65; }}
242
+ .gradio-container {{ max-width:100% !important; min-height:100vh; margin:0 !important; padding:0 !important; display:flex; flex-direction:column; }}
243
+ .app-header-alpha {{ padding: 3rem 1.5rem 4rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }}
244
+ .app-header-alpha h1 {{ font-size: 2.4em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }}
245
+ .app-header-alpha p {{ font-size: 1.1em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }}
246
+ .main-content-panel-alpha {{ padding: 1.8rem 1.5rem; max-width: 680px; margin: -2.5rem auto 2rem auto; width: 90%; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); position:relative; z-index:10; }}
247
+ @media (max-width: 768px) {{ .main-content-panel-alpha {{ width: 95%; padding: 1.5rem 1rem; margin-top: -2rem; }} .app-header-alpha h1 {{font-size:2em;}} .app-header-alpha p {{font-size:1em;}} }}
248
+ footer {{display:none !important;}}
249
+
250
+ .gr-button.generate-button-final {{ background: var(--app-button-bg) !important; color: white !important; border:none !important; border-radius: var(--radius-input) !important; padding: 0.8rem 1.5rem !important; font-weight: 700 !important; font-size:1.05em !important; transition: all 0.3s ease; box-shadow: var(--shadow-button); width:100%; margin-top:1.5rem !important; }}
251
+ .gr-button.generate-button-final:hover {{ filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 6px 12px -3px rgba(41,121,255,0.6);}}
252
+ .gr-input > label + div > textarea, .gr-dropdown > label + div > div > input, .gr-dropdown > label + div > div > select, .gr-textbox > label + div > textarea, .gr-file > label + div {{ border-radius: var(--radius-input) !important; border: 1px solid var(--app-border-color) !important; background-color: var(--app-input-bg) !important; box-shadow: inset 0 1px 2px rgba(0,0,0,0.05); padding: 0.75rem !important; }}
253
+ .gr-file > label + div {{ text-align:center; border-style: dashed !important; }} /* ظاهر آپلود فایل */
254
+ .gr-input > label + div > textarea:focus, .gr-dropdown > label + div > div > input:focus, .gr-textbox > label + div > textarea:focus {{ border-color: var(--app-button-bg) !important; box-shadow: 0 0 0 3px rgba(41,121,255,0.2) !important; }}
255
+ label > .label-text {{ font-weight: 700 !important; color: var(--app-text-primary) !important; font-size: 0.95em !important; margin-bottom: 0.5rem !important; }}
256
+ .section-title-main-alpha {{ font-size: 1.1em; color: var(--app-text-secondary); margin-bottom:1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--app-border-color); font-weight:500; text-align:right; }}
257
+ /* آیکون‌ها قبل از لیبل‌ها در تصویر نمونه */
258
+ label > .label-text::before {{ margin-left: 8px; vertical-align: middle; opacity: 0.7; }}
259
+ label[for*="text_input_main_alpha_v3"] > .label-text::before {{ content: '📝'; }}
260
+ label[for*="speech_prompt_alpha_v3"] > .label-text::before {{ content: '🗣️'; }}
261
+ label[for*="speaker_voice_alpha_v3"] > .label-text::before {{ content: '🎤'; }}
262
+ label[for*="temperature_slider_alpha_v3"] > .label-text::before {{ content: '🌡️'; }}
263
+
264
+ #output_audio_player_alpha_v3 audio {{ width: 100%; border-radius: var(--radius-input); margin-top:0.8rem; }}
265
+ .temp_description_class_alpha_v3 {{ font-size: 0.85em; color: #777; margin-top: -0.4rem; margin-bottom: 1rem; }}
266
+ .app-footer-final {{text-align:center;font-size:0.9em;color: var(--app-text-secondary);opacity:0.8; margin-top:3rem;padding:1.5rem 0; border-top:1px solid var(--app-border-color);}}
267
  """
 
268
 
269
+ alpha_header_html_v3 = """
270
+ <div class='app-header-alpha'>
271
+ <h1>Alpha TTS</h1>
272
+ <p>جادوی تبدیل متن به صدا در دستان شما</p>
273
+ </div>
274
+ """ # بدون آیکون موشک، چون آیکون‌های تصویری نیاز به فایل دارند
275
 
276
+ with gr.Blocks(theme=gr.themes.Base(font=[gr.themes.GoogleFont("Vazirmatn")]), css=custom_css_inspired_by_image, title="آلفا TTS") as demo:
277
+ gr.HTML(alpha_header_html_v3)
 
 
 
 
 
 
278
 
279
+ with gr.Column(elem_classes=["main-content-panel-alpha"]):
280
+ # عنوان بخش حذف شد تا فضا کمتر گرفته شود و شبیه تصویر شود
281
+ # gr.Markdown("<h3 class='section-title-main-alpha'>ورودی متن و تنظیمات</h3>")
282
+
283
+ use_file_input_cb = gr.Checkbox(label="📄 استفاده از فایل متنی (.txt)", value=False, elem_id="use_file_cb_alpha_v3")
284
+ uploaded_file_input = gr.File(
285
+ label=" ", # لیبل خالی برای ظاهر شبیه تصویر
286
+ file_types=['.txt'],
287
+ visible=False,
288
+ elem_id="file_uploader_alpha_main_v3"
289
+ )
290
+ text_to_speak_tb = gr.Textbox(
291
+ label="متن فارسی برای تبدیل", # لیبل کوتاه‌تر
292
+ placeholder="مثال: سلام، فردا هوا چطور است؟", # placeholder از تصویر
293
+ lines=5,
294
+ value="", # خالی در ابتدا
295
+ visible=True,
296
+ elem_id="text_input_main_alpha_v3"
297
+ )
298
+ use_file_input_cb.change(
299
+ fn=lambda x: (gr.update(visible=x, label=" " if x else "متن فارسی برای تبدیل"), gr.update(visible=not x)), # تغییر لیبل فایل هنگام نمایش
300
+ inputs=use_file_input_cb,
301
+ outputs=[uploaded_file_input, text_to_speak_tb]
302
+ )
 
 
 
 
 
 
 
 
 
 
303
 
304
+ speech_prompt_tb = gr.Textbox(
305
+ label="سبک گفتار (اختیاری)",
306
+ placeholder="مثال: با لحنی شاد و پرانرژی", # placeholder کوتاه‌تر
307
+ value="با لحنی دوستانه و رسا صحبت کن.",
308
+ lines=2, elem_id="speech_prompt_alpha_v3"
309
+ )
310
+ speaker_voice_dd = gr.Dropdown(
311
+ SPEAKER_VOICES, label="انتخاب گوینده و لهجه", value="Charon", elem_id="speaker_voice_alpha_v3"
312
+ )
313
+ temperature_slider = gr.Slider(
314
+ minimum=0.1, maximum=1.5, step=0.05, value=0.9, label="میزان خلاقیت صدا",
315
+ elem_id="temperature_slider_alpha_v3"
316
+ )
317
+ gr.Markdown("<p class='temp_description_class_alpha_v3'>مقادیر بالاتر = تنوع بیشتر، مقادیر پایین‌تر = یکنواختی بیشتر.</p>")
318
 
319
+ # فیلد نام فایل خروجی حذف شد
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ generate_button = gr.Button("🚀 تولید و پخش صدا", elem_classes=["generate-button-final"], elem_id="generate_button_alpha_v3")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
+ # عنوان بخش نتیجه حذف شد
324
+ output_audio = gr.Audio(label=" ", type="filepath", elem_id="output_audio_player_alpha_v3") # لیبل خالی
325
+
326
+ generate_button.click(
327
+ fn=gradio_tts_interface,
328
+ inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ], # output_filename_base_tb حذف شد
329
+ outputs=[output_audio]
330
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ # بخش Examples با استایل ساده‌تر و بدون عنوان جداگانه برای تطابق با ظاهر ساده شده
333
+ # برای شباهت بیشتر به تصویر، می‌توان Examples را هم حذف کرد یا بسیار ساده‌تر کرد.
334
+ # فعلا نگه می‌داریم اما با ظاهر ساده‌تر.
335
+ gr.Markdown("<h3 class='section-title-main-alpha' style='margin-top:2.5rem; text-align:center; border-bottom:none;'>نمونه‌های کاربردی</h3>", elem_id="examples_section_title_v3")
336
+ gr.Examples(
337
+ examples=[
338
+ [False, None, "سلام بر شما، امیدوارم روز خوبی داشته باشید.", "با لحنی گرم و صمیمی.", "Zephyr", 0.85],
339
+ [False, None, "این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.", "با صدایی طبیعی و روان.", "Charon", 0.9],
340
+ ],
341
+ # ورودی‌ها باید با تابع اصلی مطابقت داشته باشند، output_filename_base_tb حذف شده
342
+ inputs=[ use_file_input_cb, uploaded_file_input, text_to_speak_tb, speech_prompt_tb, speaker_voice_dd, temperature_slider ],
343
+ outputs=[output_audio],
344
+ fn=gradio_tts_interface,
345
+ cache_examples=False
346
+ )
347
+ gr.Markdown("<p class='app-footer-final'>Alpha Language Learning © 2024</p>") # مشابه تصویر
348
 
349
 
350
  if __name__ == "__main__":
351
+ demo.launch()