bilalRHCH commited on
Commit
c5a9ee2
·
verified ·
1 Parent(s): 2a3d16d

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. static/index.html +92 -0
  2. static/script.js +340 -0
  3. static/style.css +523 -0
static/index.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OmniVoice Arabic TTS</title>
7
+ <!-- Fonts -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;800&family=Inter:wght@400;600&display=swap" rel="stylesheet">
11
+ <link rel="stylesheet" href="style.css">
12
+ </head>
13
+ <body>
14
+ <div class="blob blob-1"></div>
15
+ <div class="blob blob-2"></div>
16
+
17
+ <div class="container glass-panel">
18
+ <header>
19
+ <div class="header-top">
20
+ <h1>مولد الصوت العربي</h1>
21
+ <button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
22
+ <svg id="moon-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
23
+ <svg id="sun-icon" class="hidden" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
24
+ </button>
25
+ </div>
26
+ <p class="subtitle">قم بتحويل النص العربي إلى صوت نقي وواضح باستخدام OmniVoice</p>
27
+ </header>
28
+
29
+ <main>
30
+ <div class="input-group">
31
+ <label for="tts-text">النص المراد تحويله</label>
32
+ <textarea id="tts-text" placeholder="اكتب النص العربي هنا..."></textarea>
33
+ </div>
34
+
35
+ <div class="input-row">
36
+ <div class="input-group relative-group">
37
+ <label>الصوت</label>
38
+ <button type="button" id="custom-voice-btn" class="custom-select-btn">
39
+ <div class="voice-btn-content" id="selected-voice-display">
40
+ <div class="avatar" style="background-color: #4A90E2;">ت</div>
41
+ <div class="voice-info">
42
+ <span class="voice-name">تلقائي (Auto)</span>
43
+ <span class="voice-gender" style="opacity: 0.7; font-size: 0.85em;">ذكاء اصطناعي</span>
44
+ </div>
45
+ </div>
46
+ <svg class="dropdown-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
47
+ </button>
48
+
49
+ <div id="voice-selector-modal" class="voice-selector-modal hidden">
50
+ <div class="modal-header">
51
+ <span>اختر صوتاً</span>
52
+ <button type="button" id="close-modal-btn" class="icon-btn close-btn">
53
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
54
+ </button>
55
+ </div>
56
+ <div class="voice-list" id="voice-list-container">
57
+ <!-- Managed by JS -->
58
+ </div>
59
+ </div>
60
+ </div>
61
+ <div class="input-group">
62
+ <label for="tts-speed">سرعة القراءة</label>
63
+ <input type="number" id="tts-speed" min="0.5" max="2.0" step="0.1" value="1.0">
64
+ </div>
65
+ </div>
66
+
67
+ <button id="synthesize-btn" class="primary-btn">
68
+ <span class="btn-text">توليد الصوت</span>
69
+ <div class="spinner hidden"></div>
70
+ </button>
71
+
72
+ <div id="progress-container" class="hidden" style="margin-top: 1.5rem;">
73
+ <div class="progress-bar-bg" style="width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; margin-bottom: 0.5rem;">
74
+ <div id="progress-fill" style="width: 0%; height: 100%; background: var(--primary); transition: width 0.3s ease;"></div>
75
+ </div>
76
+ <p id="progress-text" style="font-size: 0.9rem; text-align: center; color: var(--text-secondary);">جاري المعالجة... 0%</p>
77
+ </div>
78
+
79
+ <div id="result-container" class="hidden">
80
+ <div class="audio-wrapper">
81
+ <audio id="audio-player" controls></audio>
82
+ </div>
83
+ <button id="download-btn" class="secondary-btn">تحميل الصوت</button>
84
+ </div>
85
+
86
+ <div id="error-message" class="error-toast hidden"></div>
87
+ </main>
88
+ </div>
89
+
90
+ <script src="script.js"></script>
91
+ </body>
92
+ </html>
static/script.js ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const HF_API = "https://bilalrhch-arabic-tts-api.hf.space";
3
+
4
+ // ─────────────────────────────────────────────
5
+ // Voice definitions
6
+ // Add more voices here by uploading <id>.wav + <id>.txt to the HF /voices folder
7
+ // ─────────────────────────────────────────────
8
+ const availableVoices = [
9
+ { id: "Auto", name: "تلقائي", nameEn: "Auto", gender: "ذكاء اصطناعي", color: "#4A90E2" },
10
+ { id: "tariq", name: "طارق", nameEn: "Tariq", gender: "رجل", color: "#10B981" },
11
+ { id: "omar", name: "عمر", nameEn: "Omar", gender: "رجل", color: "#F59E0B" },
12
+ { id: "layla", name: "ليلى", nameEn: "Layla", gender: "امرأة", color: "#E11D48" },
13
+ { id: "nour", name: "نور", nameEn: "Nour", gender: "امرأة", color: "#8B5CF6" },
14
+ ];
15
+
16
+ let selectedVoiceId = "Auto";
17
+ let previewAudio = null;
18
+ let playingVoiceId = null;
19
+
20
+ // ─────────────────────────────────────────────
21
+ // Theme Toggle
22
+ // ─────────────────────────────────────────────
23
+ const themeToggle = document.getElementById('theme-toggle');
24
+ const moonIcon = document.getElementById('moon-icon');
25
+ const sunIcon = document.getElementById('sun-icon');
26
+
27
+ const savedTheme = localStorage.getItem('theme');
28
+ if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
29
+ document.documentElement.setAttribute('data-theme', 'dark');
30
+ moonIcon.classList.add('hidden');
31
+ sunIcon.classList.remove('hidden');
32
+ }
33
+
34
+ themeToggle.addEventListener('click', () => {
35
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
36
+ if (isDark) {
37
+ document.documentElement.removeAttribute('data-theme');
38
+ localStorage.setItem('theme', 'light');
39
+ moonIcon.classList.remove('hidden');
40
+ sunIcon.classList.add('hidden');
41
+ } else {
42
+ document.documentElement.setAttribute('data-theme', 'dark');
43
+ localStorage.setItem('theme', 'dark');
44
+ moonIcon.classList.add('hidden');
45
+ sunIcon.classList.remove('hidden');
46
+ }
47
+ });
48
+
49
+ // ─────────────────────────────────────────────
50
+ // Voice Selector Modal
51
+ // ─────────────────────────────────────────────
52
+ const voiceBtn = document.getElementById('custom-voice-btn');
53
+ const voiceModal = document.getElementById('voice-selector-modal');
54
+ const closeModalBtn = document.getElementById('close-modal-btn');
55
+ const voiceListEl = document.getElementById('voice-list-container');
56
+
57
+ function getInitial(voice) {
58
+ return voice.name.charAt(0);
59
+ }
60
+
61
+ function renderVoiceList() {
62
+ voiceListEl.innerHTML = '';
63
+ availableVoices.forEach(voice => {
64
+ const isActive = voice.id === selectedVoiceId;
65
+
66
+ const item = document.createElement('div');
67
+ item.className = 'voice-item' + (isActive ? ' active' : '');
68
+ item.dataset.voiceId = voice.id;
69
+
70
+ item.innerHTML = `
71
+ <div class="voice-item-left">
72
+ <div class="avatar" style="background-color: ${voice.color}">${getInitial(voice)}</div>
73
+ <div class="voice-info">
74
+ <span class="voice-name">${voice.name}</span>
75
+ <span class="voice-gender">${voice.gender}</span>
76
+ </div>
77
+ </div>
78
+ <div class="voice-item-right">
79
+ ${voice.id !== 'Auto' ? `
80
+ <button type="button" class="preview-btn" data-voice-id="${voice.id}" title="معاينة الصوت">
81
+ <div class="preview-audio-anim hidden" id="eq-${voice.id}">
82
+ <span></span><span></span><span></span>
83
+ </div>
84
+ <svg class="play-icon" id="play-icon-${voice.id}" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
85
+ <path d="M8 5v14l11-7z"/>
86
+ </svg>
87
+ </button>` : ''}
88
+ <svg class="checkmark" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
89
+ </div>
90
+ `;
91
+
92
+ // Select voice on row click (not the preview button)
93
+ item.addEventListener('click', (e) => {
94
+ if (e.target.closest('.preview-btn')) return;
95
+ selectVoice(voice);
96
+ closeModal();
97
+ });
98
+
99
+ voiceListEl.appendChild(item);
100
+ });
101
+
102
+ // Attach preview button handlers
103
+ voiceListEl.querySelectorAll('.preview-btn').forEach(btn => {
104
+ btn.addEventListener('click', (e) => {
105
+ e.stopPropagation();
106
+ const vId = btn.dataset.voiceId;
107
+ togglePreview(vId);
108
+ });
109
+ });
110
+ }
111
+
112
+ function selectVoice(voice) {
113
+ selectedVoiceId = voice.id;
114
+
115
+ // Update trigger button display
116
+ document.querySelector('#selected-voice-display .avatar').style.backgroundColor = voice.color;
117
+ document.querySelector('#selected-voice-display .avatar').textContent = getInitial(voice);
118
+ document.querySelector('#selected-voice-display .voice-name').textContent = voice.name;
119
+ document.querySelector('#selected-voice-display .voice-gender').textContent = voice.gender;
120
+
121
+ // Re-render to update active checkmark
122
+ renderVoiceList();
123
+ }
124
+
125
+ function openModal() {
126
+ renderVoiceList();
127
+ voiceModal.classList.remove('hidden');
128
+ voiceBtn.classList.add('open');
129
+ }
130
+
131
+ function closeModal() {
132
+ voiceModal.classList.add('hidden');
133
+ voiceBtn.classList.remove('open');
134
+ }
135
+
136
+ voiceBtn.addEventListener('click', () => {
137
+ if (voiceModal.classList.contains('hidden')) {
138
+ openModal();
139
+ } else {
140
+ closeModal();
141
+ }
142
+ });
143
+
144
+ closeModalBtn.addEventListener('click', closeModal);
145
+
146
+ // Close modal when clicking outside
147
+ document.addEventListener('click', (e) => {
148
+ if (!voiceModal.classList.contains('hidden') &&
149
+ !voiceBtn.contains(e.target) &&
150
+ !voiceModal.contains(e.target)) {
151
+ closeModal();
152
+ }
153
+ });
154
+
155
+ // ─────────────────────────────────────────────
156
+ // Audio Preview
157
+ // ─────────────────────────────────────────────
158
+ function togglePreview(voiceId) {
159
+ if (playingVoiceId === voiceId) {
160
+ // Stop the current preview
161
+ stopPreview();
162
+ return;
163
+ }
164
+
165
+ // Stop previous if any
166
+ stopPreview();
167
+
168
+ const url = `${HF_API}/voices/${voiceId}.wav`;
169
+ previewAudio = new Audio(url);
170
+ playingVoiceId = voiceId;
171
+
172
+ // Show EQ animation, hide play icon
173
+ const eq = document.getElementById(`eq-${voiceId}`);
174
+ const playIcon = document.getElementById(`play-icon-${voiceId}`);
175
+ if (eq) eq.classList.remove('hidden');
176
+ if (playIcon) playIcon.classList.add('hidden');
177
+
178
+ previewAudio.play().catch(() => {
179
+ // Voice file not uploaded yet — silently fail
180
+ stopPreview();
181
+ });
182
+
183
+ previewAudio.addEventListener('ended', stopPreview);
184
+ }
185
+
186
+ function stopPreview() {
187
+ if (previewAudio) {
188
+ previewAudio.pause();
189
+ previewAudio.currentTime = 0;
190
+ previewAudio = null;
191
+ }
192
+
193
+ if (playingVoiceId) {
194
+ const eq = document.getElementById(`eq-${playingVoiceId}`);
195
+ const playIcon = document.getElementById(`play-icon-${playingVoiceId}`);
196
+ if (eq) eq.classList.add('hidden');
197
+ if (playIcon) playIcon.classList.remove('hidden');
198
+ playingVoiceId = null;
199
+ }
200
+ }
201
+
202
+ // ─────────────────────────────────────────────
203
+ // TTS Logic
204
+ // ─────────────────────────────────────────────
205
+ const synthesizeBtn = document.getElementById('synthesize-btn');
206
+ const btnText = synthesizeBtn.querySelector('.btn-text');
207
+ const spinner = synthesizeBtn.querySelector('.spinner');
208
+ const textInput = document.getElementById('tts-text');
209
+ const speedInput = document.getElementById('tts-speed');
210
+ const resultContainer = document.getElementById('result-container');
211
+ const audioPlayer = document.getElementById('audio-player');
212
+ const downloadBtn = document.getElementById('download-btn');
213
+ const errorMessage = document.getElementById('error-message');
214
+
215
+ let currentAudioUrl = null;
216
+
217
+ synthesizeBtn.addEventListener('click', async () => {
218
+ const text = textInput.value.trim();
219
+ if (!text) {
220
+ showError("الرجاء إدخال النص أول��ً");
221
+ return;
222
+ }
223
+
224
+ hideError();
225
+ resultContainer.classList.add('hidden');
226
+ setLoading(true);
227
+ stopPreview();
228
+ closeModal();
229
+
230
+ try {
231
+ const response = await fetch(`${HF_API}/synthesize`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({
235
+ text: text,
236
+ voice: selectedVoiceId,
237
+ speed: parseFloat(speedInput.value) || 1.0
238
+ })
239
+ });
240
+
241
+ if (!response.ok) {
242
+ const errorData = await response.json().catch(() => null);
243
+ throw new Error(errorData?.detail || `HTTP Error: ${response.status}`);
244
+ }
245
+
246
+ const data = await response.json();
247
+ const taskId = data.task_id;
248
+
249
+ document.getElementById('progress-container').classList.remove('hidden');
250
+ pollTaskStatus(taskId);
251
+
252
+ } catch (error) {
253
+ console.error("Synthesis error:", error);
254
+ showError(`حدث خطأ أثناء التوليد: ${error.message}`);
255
+ setLoading(false);
256
+ }
257
+ });
258
+
259
+ // ─────────────────────────────────────────────
260
+ // Status Polling
261
+ // ─────────────────────────────────────────────
262
+ async function pollTaskStatus(taskId) {
263
+ try {
264
+ const statusRes = await fetch(`${HF_API}/status/${taskId}`);
265
+ if (!statusRes.ok) {
266
+ if (statusRes.status === 404 || statusRes.status === 503) {
267
+ throw new Error(`تعذر جلب الحالة (HTTP ${statusRes.status}). الخادم قيد إعادة التشغيل، يرجى المحاولة بعد دقيقة.`);
268
+ }
269
+ throw new Error(`Could not fetch status: HTTP ${statusRes.status}`);
270
+ }
271
+
272
+ const state = await statusRes.json();
273
+
274
+ document.getElementById('progress-fill').style.width = `${state.progress}%`;
275
+
276
+ let statusText = `جاري المعالجة... ${state.progress}%`;
277
+ if (state.status === "stitching") statusText = "جاري تجميع الملفات الصوتية...";
278
+ document.getElementById('progress-text').textContent = statusText;
279
+
280
+ if (state.status === "completed") {
281
+ document.getElementById('progress-container').classList.add('hidden');
282
+ if (currentAudioUrl && currentAudioUrl.startsWith('blob:')) {
283
+ URL.revokeObjectURL(currentAudioUrl);
284
+ }
285
+ currentAudioUrl = `${HF_API}/${state.download_url}`;
286
+ audioPlayer.src = currentAudioUrl;
287
+ resultContainer.classList.remove('hidden');
288
+ setLoading(false);
289
+ audioPlayer.play().catch(e => console.log('Autoplay blocked', e));
290
+
291
+ } else if (state.status === "failed") {
292
+ throw new Error(state.error || "Unknown background generation error");
293
+ } else {
294
+ setTimeout(() => pollTaskStatus(taskId), 2000);
295
+ }
296
+
297
+ } catch (error) {
298
+ console.error("Polling error:", error);
299
+ document.getElementById('progress-container').classList.add('hidden');
300
+ showError(`انقطع الاتصال أو حدث خطأ: ${error.message}`);
301
+ setLoading(false);
302
+ }
303
+ }
304
+
305
+ // ─────────────────────────────────────────────
306
+ // Download
307
+ // ─────────────────────────────────────────────
308
+ downloadBtn.addEventListener('click', () => {
309
+ if (!currentAudioUrl) return;
310
+ const a = document.createElement('a');
311
+ a.href = currentAudioUrl;
312
+ a.download = `OmniVoice_Arabic_${new Date().getTime()}.wav`;
313
+ document.body.appendChild(a);
314
+ a.click();
315
+ document.body.removeChild(a);
316
+ });
317
+
318
+ // ─────────────────────────────────────────────
319
+ // Utils
320
+ // ─────────────────────────────────────────────
321
+ function setLoading(isLoading) {
322
+ synthesizeBtn.disabled = isLoading;
323
+ if (isLoading) {
324
+ btnText.classList.add('hidden');
325
+ spinner.classList.remove('hidden');
326
+ } else {
327
+ btnText.classList.remove('hidden');
328
+ spinner.classList.add('hidden');
329
+ }
330
+ }
331
+
332
+ function showError(msg) {
333
+ errorMessage.textContent = msg;
334
+ errorMessage.classList.remove('hidden');
335
+ }
336
+
337
+ function hideError() {
338
+ errorMessage.classList.add('hidden');
339
+ }
340
+ });
static/style.css ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Light Mode Tokens */
3
+ --bg-color: #f3f4f6;
4
+ --text-primary: #1f2937;
5
+ --text-secondary: #4b5563;
6
+ --panel-bg: rgba(255, 255, 255, 0.7);
7
+ --panel-border: rgba(255, 255, 255, 0.4);
8
+ --input-bg: rgba(255, 255, 255, 0.9);
9
+ --input-border: #d1d5db;
10
+ --accent: #4f46e5;
11
+ --accent-hover: #4338ca;
12
+ --blob-1: rgba(99, 102, 241, 0.4);
13
+ --blob-2: rgba(168, 85, 247, 0.4);
14
+ --shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.1);
15
+ }
16
+
17
+ [data-theme="dark"] {
18
+ /* Dark Mode Tokens */
19
+ --bg-color: #0d1117;
20
+ --text-primary: #f9fafb;
21
+ --text-secondary: #9ca3af;
22
+ --panel-bg: rgba(30, 41, 59, 0.7);
23
+ --panel-border: rgba(255, 255, 255, 0.1);
24
+ --input-bg: rgba(15, 23, 42, 0.8);
25
+ --input-border: #334155;
26
+ --accent: #6366f1;
27
+ --accent-hover: #818cf8;
28
+ --blob-1: rgba(99, 102, 241, 0.2);
29
+ --blob-2: rgba(168, 85, 247, 0.2);
30
+ --shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
31
+ }
32
+
33
+ * {
34
+ margin: 0;
35
+ padding: 0;
36
+ box-sizing: border-box;
37
+ }
38
+
39
+ body {
40
+ background-color: var(--bg-color);
41
+ color: var(--text-primary);
42
+ font-family: 'Cairo', 'Inter', sans-serif;
43
+ min-height: 100vh;
44
+ display: flex;
45
+ justify-content: center;
46
+ align-items: center;
47
+ overflow-x: hidden;
48
+ position: relative;
49
+ transition: background-color 0.5s ease, color 0.5s ease;
50
+ }
51
+
52
+ /* Background Blobs (Dynamic Aesthetic) */
53
+ .blob {
54
+ position: absolute;
55
+ filter: blur(80px);
56
+ z-index: -1;
57
+ border-radius: 50%;
58
+ animation: float 10s infinite ease-in-out alternate;
59
+ }
60
+ .blob-1 {
61
+ top: -10%;
62
+ right: -10%;
63
+ width: 400px;
64
+ height: 400px;
65
+ background: var(--blob-1);
66
+ animation-delay: 0s;
67
+ }
68
+ .blob-2 {
69
+ bottom: -10%;
70
+ left: -10%;
71
+ width: 350px;
72
+ height: 350px;
73
+ background: var(--blob-2);
74
+ animation-delay: -5s;
75
+ }
76
+
77
+ @keyframes float {
78
+ 0% { transform: translateY(0) scale(1); }
79
+ 100% { transform: translateY(30px) scale(1.1); }
80
+ }
81
+
82
+ /* Glassmorphism Panel */
83
+ .glass-panel {
84
+ background: var(--panel-bg);
85
+ backdrop-filter: blur(16px);
86
+ -webkit-backdrop-filter: blur(16px);
87
+ border: 1px solid var(--panel-border);
88
+ border-radius: 24px;
89
+ padding: 40px;
90
+ width: 100%;
91
+ max-width: 600px;
92
+ box-shadow: var(--shadow);
93
+ z-index: 10;
94
+ }
95
+
96
+ header {
97
+ margin-bottom: 30px;
98
+ }
99
+
100
+ .header-top {
101
+ display: flex;
102
+ justify-content: space-between;
103
+ align-items: center;
104
+ margin-bottom: 10px;
105
+ }
106
+
107
+ h1 {
108
+ font-size: 28px;
109
+ font-weight: 800;
110
+ letter-spacing: -0.5px;
111
+ }
112
+
113
+ .subtitle {
114
+ color: var(--text-secondary);
115
+ font-size: 15px;
116
+ line-height: 1.5;
117
+ }
118
+
119
+ .icon-btn {
120
+ background: transparent;
121
+ border: none;
122
+ color: var(--text-primary);
123
+ cursor: pointer;
124
+ padding: 8px;
125
+ border-radius: 50%;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ transition: background-color 0.3s;
130
+ }
131
+
132
+ .icon-btn:hover {
133
+ background-color: var(--input-bg);
134
+ }
135
+
136
+ .hidden {
137
+ display: none !important;
138
+ }
139
+
140
+ /* Form Elements */
141
+ .input-group {
142
+ display: flex;
143
+ flex-direction: column;
144
+ margin-bottom: 20px;
145
+ gap: 8px;
146
+ }
147
+
148
+ .input-row {
149
+ display: flex;
150
+ gap: 20px;
151
+ }
152
+ .input-row .input-group {
153
+ flex: 1;
154
+ }
155
+
156
+ label {
157
+ font-size: 14px;
158
+ font-weight: 600;
159
+ color: var(--text-primary);
160
+ }
161
+
162
+ textarea, select, input {
163
+ width: 100%;
164
+ background: var(--input-bg);
165
+ border: 1px solid var(--input-border);
166
+ border-radius: 12px;
167
+ color: var(--text-primary);
168
+ font-family: inherit;
169
+ font-size: 16px;
170
+ padding: 14px;
171
+ outline: none;
172
+ transition: border-color 0.3s, box-shadow 0.3s;
173
+ }
174
+
175
+ textarea {
176
+ resize: vertical;
177
+ min-height: 120px;
178
+ }
179
+
180
+ textarea:focus, select:focus, input:focus {
181
+ border-color: var(--accent);
182
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
183
+ }
184
+
185
+ /* Primary Button */
186
+ .primary-btn {
187
+ width: 100%;
188
+ background: var(--accent);
189
+ color: white;
190
+ border: none;
191
+ padding: 16px;
192
+ border-radius: 12px;
193
+ font-size: 16px;
194
+ font-weight: 600;
195
+ font-family: inherit;
196
+ cursor: pointer;
197
+ display: flex;
198
+ justify-content: center;
199
+ align-items: center;
200
+ transition: all 0.3s ease;
201
+ box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3);
202
+ }
203
+
204
+ .primary-btn:hover {
205
+ background: var(--accent-hover);
206
+ transform: translateY(-2px);
207
+ box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
208
+ }
209
+
210
+ .primary-btn:active {
211
+ transform: translateY(0);
212
+ }
213
+
214
+ .primary-btn:disabled {
215
+ opacity: 0.7;
216
+ cursor: not-allowed;
217
+ transform: none;
218
+ }
219
+
220
+ /* Spinner */
221
+ .spinner {
222
+ border: 3px solid rgba(255,255,255,0.3);
223
+ border-top: 3px solid white;
224
+ border-radius: 50%;
225
+ width: 20px;
226
+ height: 20px;
227
+ animation: spin 1s linear infinite;
228
+ margin-right: 10px;
229
+ }
230
+
231
+ @keyframes spin {
232
+ 0% { transform: rotate(0deg); }
233
+ 100% { transform: rotate(360deg); }
234
+ }
235
+
236
+ /* Results & Audio */
237
+ #result-container {
238
+ margin-top: 30px;
239
+ padding-top: 20px;
240
+ border-top: 1px solid var(--panel-border);
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 15px;
244
+ animation: slideUp 0.5s ease;
245
+ }
246
+
247
+ @keyframes slideUp {
248
+ from { opacity: 0; transform: translateY(10px); }
249
+ to { opacity: 1; transform: translateY(0); }
250
+ }
251
+
252
+ audio {
253
+ width: 100%;
254
+ height: 40px;
255
+ outline: none;
256
+ }
257
+
258
+ .secondary-btn {
259
+ background: transparent;
260
+ border: 1px solid var(--accent);
261
+ color: var(--accent);
262
+ padding: 12px;
263
+ border-radius: 12px;
264
+ font-size: 14px;
265
+ font-weight: 600;
266
+ cursor: pointer;
267
+ transition: all 0.3s;
268
+ }
269
+
270
+ .secondary-btn:hover {
271
+ background: var(--accent);
272
+ color: white;
273
+ }
274
+
275
+ .error-toast {
276
+ margin-top: 20px;
277
+ padding: 12px;
278
+ background: rgba(239, 68, 68, 0.1);
279
+ border: 1px solid #ef4444;
280
+ color: #ef4444;
281
+ border-radius: 8px;
282
+ font-size: 14px;
283
+ text-align: center;
284
+ }
285
+
286
+ /* Responsive */
287
+ @media (max-width: 640px) {
288
+ .glass-panel {
289
+ margin: 20px;
290
+ padding: 24px;
291
+ border-radius: 20px;
292
+ }
293
+
294
+ .input-row {
295
+ flex-direction: column;
296
+ gap: 0;
297
+ }
298
+ }
299
+
300
+ /* Voice Selector Custom UI */
301
+ .relative-group {
302
+ position: relative;
303
+ }
304
+
305
+ .custom-select-btn {
306
+ width: 100%;
307
+ background: var(--input-bg);
308
+ border: 1px solid var(--input-border);
309
+ border-radius: 12px;
310
+ padding: 10px 14px;
311
+ display: flex;
312
+ justify-content: space-between;
313
+ align-items: center;
314
+ cursor: pointer;
315
+ text-align: right;
316
+ transition: all 0.3s ease;
317
+ min-height: 58px;
318
+ }
319
+
320
+ .custom-select-btn:hover {
321
+ border-color: var(--accent);
322
+ }
323
+
324
+ .voice-btn-content {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 12px;
328
+ }
329
+
330
+ .avatar {
331
+ width: 34px;
332
+ height: 34px;
333
+ border-radius: 50%;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ color: white;
338
+ font-weight: 800;
339
+ font-size: 15px;
340
+ flex-shrink: 0;
341
+ }
342
+
343
+ .voice-info {
344
+ display: flex;
345
+ flex-direction: column;
346
+ align-items: flex-start;
347
+ }
348
+
349
+ .voice-name {
350
+ font-weight: 700;
351
+ color: var(--text-primary);
352
+ font-size: 14.5px;
353
+ }
354
+
355
+ .voice-gender {
356
+ font-size: 12px;
357
+ color: var(--text-secondary);
358
+ }
359
+
360
+ .dropdown-icon {
361
+ color: var(--text-secondary);
362
+ }
363
+
364
+ /* Modal Overlay Dropdown */
365
+ .voice-selector-modal {
366
+ position: absolute;
367
+ top: 100%;
368
+ right: 0;
369
+ left: 0;
370
+ margin-top: 8px;
371
+ background: var(--panel-bg);
372
+ backdrop-filter: blur(20px);
373
+ -webkit-backdrop-filter: blur(20px);
374
+ border: 1px solid var(--panel-border);
375
+ border-radius: 16px;
376
+ box-shadow: 0 10px 40px rgba(0,0,0,0.4);
377
+ z-index: 100;
378
+ overflow: hidden;
379
+ animation: fadeIn 0.2s ease;
380
+ }
381
+
382
+ @keyframes fadeIn {
383
+ from { opacity: 0; transform: translateY(-5px); }
384
+ to { opacity: 1; transform: translateY(0); }
385
+ }
386
+
387
+ .modal-header {
388
+ display: flex;
389
+ justify-content: space-between;
390
+ align-items: center;
391
+ padding: 12px 16px;
392
+ border-bottom: 1px solid var(--panel-border);
393
+ font-weight: 600;
394
+ color: var(--text-secondary);
395
+ font-size: 13px;
396
+ background: rgba(0,0,0,0.02);
397
+ }
398
+
399
+ .close-btn {
400
+ padding: 4px;
401
+ color: var(--text-secondary);
402
+ }
403
+
404
+ .voice-list {
405
+ max-height: 280px;
406
+ overflow-y: auto;
407
+ display: flex;
408
+ flex-direction: column;
409
+ }
410
+
411
+ /* Scrollbar tweaks for modal */
412
+ .voice-list::-webkit-scrollbar {
413
+ width: 6px;
414
+ }
415
+ .voice-list::-webkit-scrollbar-track {
416
+ background: transparent;
417
+ }
418
+ .voice-list::-webkit-scrollbar-thumb {
419
+ background: var(--panel-border);
420
+ border-radius: 10px;
421
+ }
422
+
423
+ .voice-item {
424
+ display: flex;
425
+ align-items: center;
426
+ justify-content: space-between;
427
+ padding: 10px 16px;
428
+ cursor: pointer;
429
+ border-bottom: 1px solid var(--panel-border);
430
+ transition: background-color 0.2s;
431
+ }
432
+
433
+ .voice-item:last-child {
434
+ border-bottom: none;
435
+ }
436
+
437
+ .voice-item:hover, .voice-item.active {
438
+ background: rgba(99, 102, 241, 0.08);
439
+ }
440
+
441
+ .voice-item-left {
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 12px;
445
+ }
446
+
447
+ .checkmark {
448
+ color: var(--text-secondary);
449
+ display: none;
450
+ }
451
+
452
+ .voice-item.active .checkmark {
453
+ display: block;
454
+ color: var(--accent);
455
+ }
456
+
457
+ /* Audio Preview Animation */
458
+ .preview-audio-anim {
459
+ display: flex;
460
+ align-items: center;
461
+ gap: 3px;
462
+ height: 15px;
463
+ margin-right: 15px;
464
+ }
465
+
466
+ .preview-audio-anim span {
467
+ display: block;
468
+ width: 3px;
469
+ height: 3px;
470
+ background: var(--accent);
471
+ border-radius: 3px;
472
+ animation: eq 0.8s infinite alternate;
473
+ }
474
+
475
+ .preview-audio-anim span:nth-child(1) { animation-delay: 0.1s; }
476
+ .preview-audio-anim span:nth-child(2) { animation-delay: 0.3s; }
477
+ .preview-audio-anim span:nth-child(3) { animation-delay: 0.5s; }
478
+
479
+ @keyframes eq {
480
+ 0% { height: 3px; }
481
+ 100% { height: 14px; }
482
+ }
483
+
484
+ /* Voice item right-side (preview + checkmark) */
485
+ .voice-item-right {
486
+ display: flex;
487
+ align-items: center;
488
+ gap: 8px;
489
+ flex-shrink: 0;
490
+ }
491
+
492
+ /* Preview play button inside modal */
493
+ .preview-btn {
494
+ background: rgba(99, 102, 241, 0.1);
495
+ border: 1px solid rgba(99, 102, 241, 0.25);
496
+ border-radius: 50%;
497
+ width: 30px;
498
+ height: 30px;
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ cursor: pointer;
503
+ color: var(--accent);
504
+ transition: background 0.2s, transform 0.15s;
505
+ padding: 0;
506
+ }
507
+
508
+ .preview-btn:hover {
509
+ background: var(--accent);
510
+ color: white;
511
+ transform: scale(1.1);
512
+ }
513
+
514
+ /* Open state: rotate dropdown chevron */
515
+ .custom-select-btn.open .dropdown-icon {
516
+ transform: rotate(180deg);
517
+ transition: transform 0.25s ease;
518
+ }
519
+
520
+ .dropdown-icon {
521
+ transition: transform 0.25s ease;
522
+ flex-shrink: 0;
523
+ }