Hamed744 commited on
Commit
72b0cbe
·
verified ·
1 Parent(s): 13f3ed8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +335 -264
index.html CHANGED
@@ -3,230 +3,318 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>پروژه تکینگی | Alpha TTS</title>
7
  <style>
8
- @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap');
9
 
10
  :root {
11
- --app-font: 'Vazirmatn', sans-serif;
12
- --bg-color: #0d0c22;
13
- --primary-glow: #f43f9a;
14
- --secondary-glow: #a93ff4;
15
- --glass-bg: rgba(22, 21, 53, 0.4);
16
- --glass-border: rgba(255, 255, 255, 0.1);
17
- --text-primary: #f0f2f5;
18
- --text-secondary: #a0a3bd;
19
- --radius: 16px;
 
 
 
20
  }
21
 
 
 
22
  body {
23
- font-family: var(--app-font);
24
  direction: rtl;
25
  background-color: var(--bg-color);
26
- background-image: radial-gradient(circle at 10% 20%, rgba(169, 63, 244, 0.2), transparent 30%),
27
- radial-gradient(circle at 80% 90%, rgba(244, 63, 154, 0.2), transparent 40%);
28
- background-attachment: fixed;
29
  color: var(--text-primary);
30
  font-size: 16px;
31
  line-height: 1.7;
32
  margin: 0;
33
- padding: 40px 0;
34
  min-height: 100vh;
35
- -webkit-font-smoothing: antialiased;
36
- -moz-osx-font-smoothing: grayscale;
37
- overflow-x: hidden;
 
 
38
  }
39
 
40
- .container { max-width: 1300px; width: 95%; margin: 0 auto; }
41
- .app-header { text-align: center; margin-bottom: 3rem; }
42
- .app-header h1 {
43
- font-size: 3.2em;
44
- font-weight: 800;
45
- margin: 0;
46
- background: -webkit-linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
47
- -webkit-background-clip: text;
48
- -webkit-text-fill-color: transparent;
49
- text-shadow: 0 0 30px rgba(244, 63, 154, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
- .app-header p { font-size: 1.2em; color: var(--text-secondary); margin-top: 0.5rem; }
 
 
 
 
 
 
 
 
52
 
 
53
  .main-interface {
 
 
 
 
 
 
54
  display: grid;
55
- grid-template-columns: 450px 1fr;
56
- gap: 2.5rem;
57
- align-items: flex-start;
58
  }
59
 
60
- /* --- پنل شیشه‌ای --- */
61
- .glass-panel {
62
- background: var(--glass-bg);
63
- backdrop-filter: blur(15px);
64
- -webkit-backdrop-filter: blur(15px);
65
- border: 1px solid var(--glass-border);
66
- border-radius: var(--radius);
67
- padding: 2.5rem;
68
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
69
- }
70
-
71
  .form-group { margin-bottom: 2rem; }
72
- label { display: block; font-weight: 500; color: var(--text-secondary); font-size: 0.9em; margin-bottom: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px;}
73
  textarea, input[type="text"] {
74
- width: 100%; padding: 1rem; border-radius: 12px;
75
- border: 1px solid var(--glass-border);
76
- background-color: rgba(13, 12, 34, 0.7);
77
- font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; color: var(--text-primary);
78
- transition: all 0.3s ease;
 
 
 
 
79
  }
 
80
  textarea:focus, input[type="text"]:focus {
81
- outline: none; border-color: var(--primary-glow);
82
- box-shadow: 0 0 15px rgba(244, 63, 154, 0.4);
 
 
83
  }
84
 
85
- /* --- گوینده --- */
86
- #selected-speaker-card {
87
- display: flex; align-items: center; background: rgba(13, 12, 34, 0.7);
88
- border-radius: 12px; padding: 0.8rem; border: 1px solid var(--glass-border);
89
- cursor: pointer; transition: all 0.3s ease;
 
 
 
 
 
90
  }
91
- #selected-speaker-card:hover { border-color: var(--primary-glow); background: rgba(22, 21, 53, 0.7); }
92
- #selected-speaker-card img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; margin-left: 15px; border: 2px solid var(--glass-border); }
93
- #selected-speaker-info h3 { margin: 0; font-size: 1.1em; font-weight: 700; color: var(--text-primary); }
94
- #selected-speaker-info p { margin: 2px 0 0; color: var(--text-secondary); font-size: 0.85em; }
95
-
96
- /* --- دکمه اصلی --- */
97
- #generate-btn {
98
- width: 100%; padding: 1rem; font-size: 1.2em; font-weight: 700; font-family: var(--app-font);
99
- background: linear-gradient(45deg, var(--primary-glow), var(--secondary-glow));
100
- color: white; border: none; border-radius: 12px; cursor: pointer;
101
  transition: all 0.3s ease;
102
- box-shadow: 0 0 25px rgba(244, 63, 154, 0.4);
103
  }
104
- #generate-btn:hover:not(:disabled) { transform: translateY(-3px) scale(1.03); box-shadow: 0 0 40px rgba(244, 63, 154, 0.6); }
105
- #generate-btn:disabled { background: #333; color: #777; cursor: not-allowed; box-shadow: none; transform: none; }
 
 
 
 
 
 
106
 
107
- /* --- پنل خروجی و انیمیشن --- */
108
- #visualization-panel { height: 100%; min-height: 550px; display: flex; align-items: center; justify-content: center; position: relative; }
109
- #audio-player { width: 90%; max-width: 400px; display: none; color-scheme: dark; }
110
- #status-message { color: #ff6b6b; font-weight: 500; display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- /* --- انیمیشن رابط عصبی --- */
113
- #animation-container { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; align-items: center; justify-content: center; flex-direction: column; overflow: hidden; }
114
- #animation-svg { width: 300px; height: 300px; }
115
- #ai-core { fill: url(#coreGradient); animation: core-pulse 3s infinite ease-in-out; }
116
- .particle-path { fill: none; stroke: none; }
117
- .particle { fill: var(--primary-glow); r: 2.5; animation: move-particle 4s infinite linear; opacity: 0; }
118
- #particle-2 { animation-delay: -1s; } #particle-3 { animation-delay: -2s; } #particle-4 { animation-delay: -3s; }
119
- #output-wave {
120
- fill: none; stroke: url(#waveGradient); stroke-width: 4;
121
- stroke-linecap: round; stroke-dasharray: 250; stroke-dashoffset: 250;
122
- animation: draw-wave 2s infinite ease-out;
123
  }
124
- #loading-text { font-size: 1.1em; color: var(--text-secondary); margin-top: 2rem; text-shadow: 0 0 10px rgba(0,0,0,0.5); }
125
- @keyframes core-pulse {
126
- 0%, 100% { transform: scale(1); filter: drop-shadow(0 0 15px var(--primary-glow)); }
127
- 50% { transform: scale(1.05); filter: drop-shadow(0 0 25px var(--secondary-glow)); }
 
 
 
 
128
  }
129
- @keyframes move-particle {
130
- 0% { offset-distance: 0%; opacity: 1; }
131
- 80% { opacity: 1; }
132
- 100% { offset-distance: 100%; opacity: 0; }
 
 
133
  }
134
- @keyframes draw-wave {
135
- 0% { stroke-dashoffset: 250; opacity: 0; }
136
- 30% { opacity: 1; }
137
- 80% { stroke-dashoffset: 0; }
138
- 100% { stroke-dashoffset: -250; opacity: 0; }
 
 
 
 
 
 
139
  }
140
 
141
- /* --- مودال --- */
142
- #speaker-modal { background-color: rgba(13, 12, 34, 0.7); backdrop-filter: blur(10px); }
143
- .modal-content { background: var(--glass-bg); border: 1px solid var(--glass-border); }
144
- .speaker-card .speaker-visual { border: 2px solid transparent; transition: all 0.3s ease; background: rgba(13, 12, 34, 0.8); border-radius: 12px; }
145
- .speaker-card:hover .speaker-visual, .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--primary-glow); box-shadow: 0 0 15px rgba(244, 63, 154, 0.4); }
146
-
147
- /* --- Responsive --- */
148
- @media (max-width: 1100px) {
149
- .main-interface { grid-template-columns: 1fr; }
150
- #visualization-panel { min-height: 350px; margin-top: 2rem; }
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
 
 
152
  </style>
153
  </head>
154
  <body>
 
155
  <div class="container">
156
  <header class="app-header">
157
- <h1>پروژه تکینگی | Alpha</h1>
158
- <p>رابط عصبی تولید صوت</p>
159
  </header>
160
 
161
- <div class="main-interface">
162
- <div id="controls-panel" class="glass-panel">
163
  <form id="tts-form">
164
  <div class="form-group">
165
- <label for="text-input">دیتای ورودی (متن)</label>
166
- <textarea id="text-input" rows="7" placeholder="ترکیب کلمات برای سنتز صوت...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
 
 
 
167
  </div>
168
  <div class="form-group">
169
- <label for="prompt-input">پارامترهای لحن (اختیاری)</label>
170
- <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی حماسی و قدرتمند">
 
 
 
171
  </div>
172
- <div class="form-group">
173
- <label>انتخاب مدل صوتی (گوینده)</label>
174
- <div id="selected-speaker-card" role="button">
175
- <img id="selected-speaker-img" src="" alt="مدل صوتی">
176
- <div id="selected-speaker-info">
177
- <h3 id="selected-speaker-name"></h3>
178
- <p>تغییر مدل</p>
179
- </div>
180
  </div>
181
  </div>
182
- <button type="submit" id="generate-btn">سنتز و پردازش</button>
183
  </form>
184
  </div>
185
-
186
- <div id="visualization-panel" class="glass-panel">
187
- <div id="animation-container">
188
- <svg id="animation-svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
189
- <defs>
190
- <radialGradient id="coreGradient">
191
- <stop offset="0%" stop-color="#fff" />
192
- <stop offset="70%" stop-color="#f43f9a" />
193
- <stop offset="100%" stop-color="#a93ff4" />
194
- </radialGradient>
195
- <linearGradient id="waveGradient">
196
- <stop offset="0%" stop-color="#a93ff4" />
197
- <stop offset="100%" stop-color="#f43f9a" />
198
- </linearGradient>
199
- </defs>
200
- <!-- Particle Paths -->
201
- <path id="path1" class="particle-path" d="M 0,100 C 50,20 50,180 100,100" />
202
- <path id="path2" class="particle-path" d="M 200,100 C 150,180 150,20 100,100" />
203
- <!-- Particles -->
204
- <circle class="particle" id="particle-1"><animateMotion dur="4s" repeatCount="indefinite" rotate="auto"><mpath href="#path1"/></animateMotion></circle>
205
- <circle class="particle" id="particle-2"><animateMotion dur="4s" repeatCount="indefinite" rotate="auto"><mpath href="#path1"/></animateMotion></circle>
206
- <circle class="particle" id="particle-3"><animateMotion dur="4s" repeatCount="indefinite" rotate="auto"><mpath href="#path2"/></animateMotion></circle>
207
- <circle class="particle" id="particle-4"><animateMotion dur="4s" repeatCount="indefinite" rotate="auto"><mpath href="#path2"/></animateMotion></circle>
208
- <!-- AI Core -->
209
- <circle id="ai-core" cx="100" cy="100" r="30" transform-origin="center" />
210
- <!-- Output Wave -->
211
- <path id="output-wave" d="M 100,100 C 150,50 200,150 250,100" />
212
- </svg>
213
- <p id="loading-text">در حال پردازش در هسته عصبی...</p>
214
- </div>
215
- <div id="status-message"></div>
216
- <audio id="audio-player" controls></audio>
217
- </div>
218
- </div>
219
- </div>
220
 
221
- <!-- Modal (styles will be inherited) -->
222
- <div id="speaker-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease;">
223
- <div class="modal-content" style="padding: 2rem; width: 90%; max-width: 700px; max-height: 85vh; overflow-y: auto; transform: scale(0.95); transition: transform 0.3s ease;">
224
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--glass-border);">
225
- <h2>انتخاب مدل صوتی</h2>
226
- <button type="button" class="close-modal-btn" style="background: none; border: none; font-size: 2.2rem; cursor: pointer; color: var(--text-secondary);">×</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  </div>
228
- <div id="speaker-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.5rem;"></div>
229
- </div>
 
230
  </div>
231
 
232
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
@@ -239,161 +327,144 @@
239
  const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
240
  const FN_INDEX = 1;
241
 
242
- const speakers = [ { id: "Charon", name: "شهاب (مرد)" }, { id: "Zephyr", name: "آوا (زن)" }, { id: "Achird", name: "نوید (مرد)" }, { id: "Zubenelgenubi", name: "رویا (زن)" }, { id: "Vindemiatrix", name: "کیان (مرد)" }, { id: "Sadachbia", name: "پریسا (زن)" }, { id: "Sadaltager", name: "آرش (مرد)" }, { id: "Sulafat", name: "شبنم (زن)" }, { id: "Laomedeia", name: "سهیل (مرد)" }, { id: "Achernar", name: "مریم (زن)" }, { id: "Alnilam", name: "بهرام (مرد)" }, { id: "Schedar", name: "نگار (زن)" }, { id: "Gacrux", name: "فرید (مرد)" }, { id: "Pulcherrima", name: "سارا (زن)" }, { id: "Umbriel", name: "مانی (مرد)" }, { id: "Algieba", name: "آناهیتا (زن)" } ];
 
 
243
 
244
  const form = document.getElementById('tts-form');
245
  const textInput = document.getElementById('text-input');
246
  const promptInput = document.getElementById('prompt-input');
 
 
247
  const generateBtn = document.getElementById('generate-btn');
248
-
249
- const statusMessage = document.getElementById('status-message');
250
- const audioPlayer = document.getElementById('audio-player');
251
- const loadingAnimation = document.getElementById('animation-container');
252
-
253
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
254
- const speakerModal = document.getElementById('speaker-modal');
255
- const changeSpeakerBtn = document.getElementById('selected-speaker-card');
256
- const closeModalBtn = document.querySelector('.close-modal-btn');
257
- const speakerGridInModal = document.getElementById('speaker-grid');
258
- const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
259
- const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
260
 
261
- function getSpeakerById(id) {
262
- return speakers.find(s => s.id === id);
263
- }
264
 
265
- // --- FIX: Correctly determine gender for image URL ---
266
- function getImageUrl(speaker, index) {
 
 
 
 
 
267
  const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
268
- const imageIndex = (index * 11 + 7) % 100; // Use a different formula for variety
269
- return `https://randomuser.me/api/portraits/thumb/${gender}/${imageIndex}.jpg`;
270
  }
271
-
272
  function updateSelectedSpeakerDisplay(speakerId) {
273
- const speaker = getSpeakerById(speakerId);
 
274
  if (speaker) {
275
- const speakerIndex = speakers.findIndex(s => s.id === speakerId);
276
- selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex).replace('thumb/', '');
277
- selectedSpeakerNameDisplay.textContent = speaker.name;
278
  selectedSpeakerIdStorage.value = speaker.id;
 
 
 
 
 
 
 
 
279
  }
280
  }
281
 
282
- function createSpeakerCardsInModal() {
283
- speakerGridInModal.innerHTML = '';
284
  speakers.forEach((speaker, index) => {
285
- const card = document.createElement('label');
286
- card.className = 'speaker-card';
287
- card.setAttribute('for', `modal-speaker-${speaker.id}`);
288
- const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
289
- card.innerHTML = `
290
- <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" style="display:none;">
291
- <div class="speaker-visual">
292
- <img src="${getImageUrl(speaker, index)}" alt="${speaker.name}" loading="lazy" style="width: 100%; height: 120px; object-fit: cover; display: block; border-radius: 10px 10px 0 0;">
293
- <div class="speaker-name" style="padding: 0.8rem 0.5rem; font-weight: 500; color: var(--text-secondary);">${speaker.name}</div>
294
- </div>
295
  `;
296
- card.addEventListener('click', () => {
297
  updateSelectedSpeakerDisplay(speaker.id);
298
- setTimeout(() => speakerModal.classList.remove('visible'), 200);
299
  });
300
- speakerGridInModal.appendChild(card);
301
  });
302
  }
303
-
304
- changeSpeakerBtn.addEventListener('click', () => {
305
- createSpeakerCardsInModal();
306
- speakerModal.style.display = 'flex';
307
- setTimeout(() => speakerModal.style.opacity = '1', 10);
308
- });
309
- closeModalBtn.addEventListener('click', () => {
310
- speakerModal.style.opacity = '0';
311
- setTimeout(() => speakerModal.style.display = 'none', 300);
312
- });
313
-
314
- function showLoadingState() {
315
- statusMessage.style.display = 'none';
316
- audioPlayer.style.display = 'none';
317
- audioPlayer.src = '';
318
- loadingAnimation.style.display = 'flex';
319
- generateBtn.disabled = true;
320
- generateBtn.textContent = 'در حال پردازش...';
321
- }
322
 
323
- function showResultState(isSuccess, message = '') {
324
- loadingAnimation.style.display = 'none';
325
- if (isSuccess) {
326
- audioPlayer.style.display = 'block';
327
- audioPlayer.play();
328
- } else {
329
- statusMessage.textContent = message || 'یک خطای ناشناخته رخ داد.';
330
- statusMessage.style.display = 'block';
331
  }
332
- generateBtn.disabled = false;
333
- generateBtn.textContent = 'سنتز و پردازش';
334
  }
335
 
 
 
336
  async function generateAudio(event) {
337
  event.preventDefault();
338
- showLoadingState();
 
 
339
 
340
  const text = textInput.value;
341
  if (!text.trim()) {
342
- showResultState(false, 'خطا: دیتای ورودی (متن) نمی‌تواند خالی باشد.');
 
 
343
  return;
344
  }
345
 
346
- const prompt = promptInput.value;
347
- const temperature = 0.9; // Simplified for this design, can be re-added
348
- const selectedSpeaker = selectedSpeakerIdStorage.value;
349
- const sessionHash = Math.random().toString(36).substring(2);
350
-
351
  const payload = {
352
  fn_index: FN_INDEX,
353
- data: [false, null, text, prompt, selectedSpeaker, temperature],
354
- event_data: null, session_hash: sessionHash
 
355
  };
356
 
357
  try {
358
  const joinQueueResponse = await fetch(JOIN_QUEUE_URL, {
359
- method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload)
 
 
360
  });
361
- if (!joinQueueResponse.ok) throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status})`);
362
-
363
- const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
 
364
  const reader = dataResponse.body.getReader();
365
  const decoder = new TextDecoder();
366
  let finalFilePath = null;
367
- let buffer = '';
368
 
369
  while (true) {
370
  const { value, done } = await reader.read();
371
  if (done) break;
372
- buffer += decoder.decode(value, { stream: true });
373
- const lines = buffer.split('\n');
374
- buffer = lines.pop();
375
- for (const line of lines) {
376
- if (!line.startsWith('data:')) continue;
377
- try {
378
- const data = JSON.parse(line.substring(5));
379
- if (data.msg === 'process_completed' && data.success && data.output.data[0]) {
380
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
381
- break;
382
  }
383
- } catch (e) {}
 
384
  }
385
- if (finalFilePath) break;
386
  }
387
 
388
  if (finalFilePath) {
389
  audioPlayer.src = `${FILE_URL_BASE}${finalFilePath}`;
390
- showResultState(true);
391
- } else { throw new Error('فایل صوتی از سرور دریافت نشد.'); }
 
 
 
 
392
  } catch (error) {
393
- showResultState(false, `یک خطا رخ داد: ${error.message}`);
 
 
 
 
394
  }
395
  }
396
 
 
397
  updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
398
  form.addEventListener('submit', generateAudio);
399
  });
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Alpha TTS - نسخه Premium</title>
7
  <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
9
 
10
  :root {
11
+ --font-family: 'Vazirmatn', sans-serif;
12
+ --bg-color: #0c0a18;
13
+ --panel-bg: rgba(22, 19, 41, 0.45);
14
+ --panel-border: rgba(255, 255, 255, 0.15);
15
+ --text-primary: #f0f0f5;
16
+ --text-secondary: #a09cb8;
17
+ --accent-primary: #8a42e5;
18
+ --accent-secondary: #3b82f6;
19
+ --accent-glow: rgba(138, 66, 229, 0.5);
20
+ --radius-card: 28px;
21
+ --radius-input: 14px;
22
+ --shadow-button: 0 6px 20px -5px rgba(138, 66, 229, 0.6);
23
  }
24
 
25
+ *, *::before, *::after { box-sizing: border-box; }
26
+
27
  body {
28
+ font-family: var(--font-family);
29
  direction: rtl;
30
  background-color: var(--bg-color);
 
 
 
31
  color: var(--text-primary);
32
  font-size: 16px;
33
  line-height: 1.7;
34
  margin: 0;
 
35
  min-height: 100vh;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ overflow: hidden;
40
+ position: relative;
41
  }
42
 
43
+ /* --- Aurora Background Animation --- */
44
+ .aurora-background { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; }
45
+ .aurora-background::before, .aurora-background::after {
46
+ content: '';
47
+ position: absolute;
48
+ width: 800px;
49
+ height: 800px;
50
+ border-radius: 50%;
51
+ filter: blur(150px);
52
+ opacity: 0.3;
53
+ }
54
+ .aurora-background::before {
55
+ background: radial-gradient(circle, var(--accent-primary), transparent 60%);
56
+ top: -20%;
57
+ right: -20%;
58
+ animation: moveAurora1 25s infinite alternate ease-in-out;
59
+ }
60
+ .aurora-background::after {
61
+ background: radial-gradient(circle, var(--accent-secondary), transparent 60%);
62
+ bottom: -20%;
63
+ left: -20%;
64
+ animation: moveAurora2 30s infinite alternate-reverse ease-in-out;
65
+ }
66
+ @keyframes moveAurora1 {
67
+ from { transform: translate(0, 0) rotate(0deg); }
68
+ to { transform: translate(100px, 50px) rotate(45deg); }
69
  }
70
+ @keyframes moveAurora2 {
71
+ from { transform: translate(0, 0) rotate(0deg); }
72
+ to { transform: translate(-50px, -100px) rotate(-60deg); }
73
+ }
74
+
75
+ .container { max-width: 1000px; width: 95%; padding: 2rem 0; }
76
+ .app-header { text-align: center; margin-bottom: 2rem; }
77
+ .app-header h1 { font-size: 3.5em; font-weight: 800; margin: 0; color: #fff; text-shadow: 0 0 20px var(--accent-glow); }
78
+ .app-header p { font-size: 1.25em; color: var(--text-secondary); margin-top: 0.5rem; }
79
 
80
+ /* --- Glassmorphism Main Panel --- */
81
  .main-interface {
82
+ background: var(--panel-bg);
83
+ border: 1px solid var(--panel-border);
84
+ border-radius: var(--radius-card);
85
+ backdrop-filter: blur(25px);
86
+ -webkit-backdrop-filter: blur(25px);
87
+ padding: 2.5rem;
88
  display: grid;
89
+ grid-template-columns: 1.2fr 1fr;
90
+ gap: 3rem;
91
+ box-shadow: 0 20px 50px rgba(0,0,0,0.3);
92
  }
93
 
94
+ .input-column, .output-column { display: flex; flex-direction: column; }
 
 
 
 
 
 
 
 
 
 
95
  .form-group { margin-bottom: 2rem; }
96
+ label { display: flex; align-items: center; gap: 0.75rem; font-weight: 600; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; }
97
  textarea, input[type="text"] {
98
+ width: 100%;
99
+ padding: 1rem 1.2rem;
100
+ border-radius: var(--radius-input);
101
+ border: 1px solid var(--panel-border);
102
+ background-color: rgba(0,0,0,0.2);
103
+ color: var(--text-primary);
104
+ font-family: var(--font-family);
105
+ font-size: 1rem;
106
+ transition: all 0.25s ease;
107
  }
108
+ textarea { resize: vertical; min-height: 120px; }
109
  textarea:focus, input[type="text"]:focus {
110
+ outline: none;
111
+ border-color: var(--accent-primary);
112
+ background-color: rgba(0,0,0,0.3);
113
+ box-shadow: 0 0 15px var(--accent-glow);
114
  }
115
 
116
+ /* --- Speaker Carousel --- */
117
+ #speaker-carousel { display: flex; overflow-x: auto; gap: 1rem; padding-bottom: 1rem; margin-bottom: 2rem; scrollbar-width: thin; scrollbar-color: var(--accent-primary) transparent; }
118
+ #speaker-carousel::-webkit-scrollbar { height: 6px; }
119
+ #speaker-carousel::-webkit-scrollbar-thumb { background-color: var(--accent-primary); border-radius: 10px; }
120
+ .speaker-item {
121
+ cursor: pointer;
122
+ text-align: center;
123
+ flex-shrink: 0;
124
+ opacity: 0.6;
125
+ transition: all 0.3s ease;
126
  }
127
+ .speaker-item img {
128
+ width: 70px;
129
+ height: 70px;
130
+ border-radius: 50%;
131
+ border: 3px solid transparent;
132
+ object-fit: cover;
 
 
 
 
133
  transition: all 0.3s ease;
 
134
  }
135
+ .speaker-item.selected { opacity: 1; }
136
+ .speaker-item.selected img {
137
+ border-color: var(--accent-primary);
138
+ box-shadow: 0 0 15px var(--accent-glow);
139
+ transform: scale(1.1);
140
+ }
141
+ .speaker-item p { margin: 0.5rem 0 0; font-size: 0.9em; font-weight: 500; color: var(--text-secondary); transition: color 0.3s; }
142
+ .speaker-item.selected p { color: var(--text-primary); font-weight: 600; }
143
 
144
+ /* --- Output Display & Loading Animation --- */
145
+ #output-display-area {
146
+ flex-grow: 1;
147
+ background: rgba(0,0,0,0.25);
148
+ border-radius: 20px;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ position: relative;
153
+ overflow: hidden;
154
+ min-height: 250px;
155
+ }
156
+ .output-content {
157
+ display: none;
158
+ flex-direction: column;
159
+ align-items: center;
160
+ justify-content: center;
161
+ text-align: center;
162
+ width: 100%;
163
+ padding: 1rem;
164
+ }
165
+ #output-display-area[data-state="initial"] #speaker-info,
166
+ #output-display-area[data-state="loading"] #loading-animation,
167
+ #output-display-area[data-state="result"] #audio-player-container,
168
+ #output-display-area[data-state="error"] #error-message {
169
+ display: flex;
170
+ }
171
+ #speaker-info img { width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 4px solid var(--panel-border); margin-bottom: 1rem; }
172
+ #speaker-info h3 { font-size: 1.8em; margin: 0; }
173
+ #speaker-info p { color: var(--text-secondary); margin: 0.25rem 0 0; }
174
+ #audio-player { width: 90%; margin: 1rem 0; }
175
+ #error-message { color: #ff8a8a; }
176
 
177
+ /* --- Premium Loading Animation --- */
178
+ #loading-animation img {
179
+ width: 120px;
180
+ height: 120px;
181
+ border-radius: 50%;
182
+ object-fit: cover;
183
+ position: relative;
184
+ z-index: 2;
 
 
 
185
  }
186
+ .orb-container { position: absolute; width: 250px; height: 250px; }
187
+ .orb {
188
+ position: absolute;
189
+ top: 0; left: 0;
190
+ width: 100%; height: 100%;
191
+ border-radius: 50%;
192
+ border: 2px solid;
193
+ mix-blend-mode: screen;
194
  }
195
+ .orb-1 { border-color: var(--accent-primary); animation: rotate-orb 6s infinite linear; }
196
+ .orb-2 { border-color: var(--accent-secondary); animation: rotate-orb 8s infinite linear reverse; }
197
+ .orb-3 {
198
+ border-color: #5eead4;
199
+ border-style: dotted;
200
+ animation: rotate-orb 10s infinite linear;
201
  }
202
+ @keyframes rotate-orb {
203
+ from { transform: rotate(0deg) scale(0.8); opacity: 0.5; }
204
+ 50% { transform: rotate(180deg) scale(1.1); opacity: 1; }
205
+ to { transform: rotate(360deg) scale(0.8); opacity: 0.5; }
206
+ }
207
+ #loading-animation p {
208
+ margin-top: 1.5rem;
209
+ position: relative;
210
+ z-index: 2;
211
+ font-weight: 500;
212
+ text-shadow: 0 0 10px var(--bg-color);
213
  }
214
 
215
+ /* --- Slider & Button --- */
216
+ .slider-container { display: flex; align-items: center; gap: 1rem; }
217
+ input[type="range"] { flex-grow: 1; -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; }
218
+ input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: rgba(0,0,0,0.4); border-radius: 3px; }
219
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -9px; width: 24px; height: 24px; background: var(--accent-primary); border-radius: 50%; border: 4px solid var(--bg-color); box-shadow: 0 0 10px var(--accent-glow); }
220
+ #temperature-value { font-weight: 700; background-color: rgba(0,0,0,0.2); padding: 0.4rem 1rem; border-radius: 8px; min-width: 45px; text-align: center; }
221
+
222
+ #generate-btn {
223
+ grid-column: 1 / -1; /* Span across both columns */
224
+ width: 100%;
225
+ padding: 1.2rem;
226
+ font-size: 1.3em;
227
+ font-weight: 700;
228
+ font-family: var(--font-family);
229
+ background: linear-gradient(45deg, var(--accent-secondary), var(--accent-primary));
230
+ color: white;
231
+ border: none;
232
+ border-radius: var(--radius-input);
233
+ cursor: pointer;
234
+ transition: all 0.3s ease;
235
+ box-shadow: var(--shadow-button);
236
+ margin-top: 1rem;
237
  }
238
+ #generate-btn:hover:not(:disabled) { transform: translateY(-4px) scale(1.02); box-shadow: 0 10px 25px -5px rgba(138, 66, 229, 0.7); }
239
+ #generate-btn:disabled { background: #555; cursor: not-allowed; box-shadow: none; transform: none; }
240
  </style>
241
  </head>
242
  <body>
243
+ <div class="aurora-background"></div>
244
  <div class="container">
245
  <header class="app-header">
246
+ <h1>Alpha TTS</h1>
247
+ <p>نسل جدید تبدیل متن به صدا</p>
248
  </header>
249
 
250
+ <main class="main-interface">
251
+ <div class="input-column">
252
  <form id="tts-form">
253
  <div class="form-group">
254
+ <label for="text-input">
255
+ <svg 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="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
256
+ متن اصلی
257
+ </label>
258
+ <textarea id="text-input" rows="6" placeholder="متن خود را برای تبدیل به صدا اینجا وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
259
  </div>
260
  <div class="form-group">
261
+ <label for="prompt-input">
262
+ <svg 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="m12 15-3-3a3 3 0 0 1 3-3 3 3 0 0 1 3 3l-3 3"></path><path d="m19 9-3 3"></path><path d="M5 15l3-3"></path></svg>
263
+ راهنمای لحن (اختیاری)
264
+ </label>
265
+ <input type="text" id="prompt-input" value="با صدایی آرام و دلنشین." placeholder="مثال: با لحنی شاد و پرانرژی">
266
  </div>
267
+ <div class="form-group">
268
+ <label for="temperature-slider">
269
+ <svg 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="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"></path></svg>
270
+ میزان خلاقیت
271
+ </label>
272
+ <div class="slider-container">
273
+ <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
274
+ <span id="temperature-value">0.9</span>
275
  </div>
276
  </div>
 
277
  </form>
278
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
+ <div class="output-column">
281
+ <label>انتخاب گوینده</label>
282
+ <div id="speaker-carousel"></div>
283
+
284
+ <div id="output-display-area" data-state="initial">
285
+ <!-- State 1: Initial Speaker Info -->
286
+ <div id="speaker-info" class="output-content">
287
+ <img id="selected-speaker-img" src="" alt="عکس گوینده">
288
+ <h3 id="selected-speaker-name"></h3>
289
+ <p>گوینده م��تخب شما</p>
290
+ </div>
291
+
292
+ <!-- State 2: Loading Animation -->
293
+ <div id="loading-animation" class="output-content">
294
+ <div class="orb-container">
295
+ <div class="orb orb-1"></div>
296
+ <div class="orb orb-2"></div>
297
+ <div class="orb orb-3"></div>
298
+ </div>
299
+ <img id="loading-speaker-img" src="" alt="عکس گوینده در حال پردازش">
300
+ <p>در حال خلق صدا با هوش مصنوعی آلفا...</p>
301
+ </div>
302
+
303
+ <!-- State 3: Result -->
304
+ <div id="audio-player-container" class="output-content">
305
+ <p>فایل صوتی شما آماده است!</p>
306
+ <audio id="audio-player" controls></audio>
307
+ </div>
308
+
309
+ <!-- State 4: Error -->
310
+ <div id="error-message" class="output-content">
311
+ <p></p>
312
+ </div>
313
+ </div>
314
  </div>
315
+
316
+ <button type="submit" id="generate-btn" form="tts-form">✨ خلق صدا</button>
317
+ </main>
318
  </div>
319
 
320
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
 
327
  const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
328
  const FN_INDEX = 1;
329
 
330
+ const speakers = [
331
+ { id: "Zephyr", name: "آوا (زن)" }, { id: "Charon", name: "شهاب (مرد)" }, { id: "Zubenelgenubi", name: "رویا (زن)" }, { id: "Achird", name: "نوید (مرد)" }, { id: "Sadachbia", name: "پریسا (زن)" }, { id: "Vindemiatrix", name: "کیان (مرد)" }, { id: "Sulafat", name: "شبنم (زن)" }, { id: "Sadaltager", name: "آرش (مرد)" }, { id: "Achernar", name: "مریم (زن)" }, { id: "Laomedeia", name: "سهیل (مرد)" }, { id: "Schedar", name: "نگار (زن)" }, { id: "Alnilam", name: "بهرام (مرد)" }, { id: "Pulcherrima", name: "سارا (زن)" }
332
+ ];
333
 
334
  const form = document.getElementById('tts-form');
335
  const textInput = document.getElementById('text-input');
336
  const promptInput = document.getElementById('prompt-input');
337
+ const tempSlider = document.getElementById('temperature-slider');
338
+ const tempValueSpan = document.getElementById('temperature-value');
339
  const generateBtn = document.getElementById('generate-btn');
 
 
 
 
 
340
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
 
 
 
 
 
 
341
 
342
+ const speakerCarousel = document.getElementById('speaker-carousel');
343
+ const outputDisplayArea = document.getElementById('output-display-area');
 
344
 
345
+ const selectedSpeakerImg = document.getElementById('selected-speaker-img');
346
+ const loadingSpeakerImg = document.getElementById('loading-speaker-img');
347
+ const selectedSpeakerName = document.getElementById('selected-speaker-name');
348
+ const audioPlayer = document.getElementById('audio-player');
349
+ const errorMessageP = document.querySelector('#error-message p');
350
+
351
+ function getImageUrl(speaker, index, size = 'med') {
352
  const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
353
+ const imageIndex = (index * 5 + 10) % 100; // Formula for variety
354
+ return `https://randomuser.me/api/portraits/${size}/${gender}/${imageIndex}.jpg`;
355
  }
356
+
357
  function updateSelectedSpeakerDisplay(speakerId) {
358
+ const speaker = speakers.find(s => s.id === speakerId);
359
+ const speakerIndex = speakers.findIndex(s => s.id === speakerId);
360
  if (speaker) {
 
 
 
361
  selectedSpeakerIdStorage.value = speaker.id;
362
+ selectedSpeakerImg.src = getImageUrl(speaker, speakerIndex, 'men'); // Large image
363
+ loadingSpeakerImg.src = getImageUrl(speaker, speakerIndex, 'men'); // Image for loading animation
364
+ selectedSpeakerName.textContent = speaker.name;
365
+
366
+ // Update carousel selection
367
+ document.querySelectorAll('.speaker-item').forEach(item => {
368
+ item.classList.toggle('selected', item.dataset.speakerId === speakerId);
369
+ });
370
  }
371
  }
372
 
373
+ function populateSpeakerCarousel() {
374
+ speakerCarousel.innerHTML = '';
375
  speakers.forEach((speaker, index) => {
376
+ const item = document.createElement('div');
377
+ item.className = 'speaker-item';
378
+ item.dataset.speakerId = speaker.id;
379
+ item.innerHTML = `
380
+ <img src="${getImageUrl(speaker, index, 'thumb')}" alt="${speaker.name}">
381
+ <p>${speaker.name.split(' ')[0]}</p>
 
 
 
 
382
  `;
383
+ item.addEventListener('click', () => {
384
  updateSelectedSpeakerDisplay(speaker.id);
385
+ outputDisplayArea.dataset.state = 'initial'; // Reset state on speaker change
386
  });
387
+ speakerCarousel.appendChild(item);
388
  });
389
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
+ function setOutputState(state, message = '') {
392
+ outputDisplayArea.dataset.state = state;
393
+ if (state === 'error') {
394
+ errorMessageP.textContent = message;
 
 
 
 
395
  }
 
 
396
  }
397
 
398
+ tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
399
+
400
  async function generateAudio(event) {
401
  event.preventDefault();
402
+ setOutputState('loading');
403
+ generateBtn.disabled = true;
404
+ generateBtn.textContent = 'در حال پردازش...';
405
 
406
  const text = textInput.value;
407
  if (!text.trim()) {
408
+ setOutputState('error', 'خطا: متن ورودی نمی‌تواند خالی باشد.');
409
+ generateBtn.disabled = false;
410
+ generateBtn.textContent = '✨ خلق صدا';
411
  return;
412
  }
413
 
 
 
 
 
 
414
  const payload = {
415
  fn_index: FN_INDEX,
416
+ data: [false, null, text, promptInput.value, selectedSpeakerIdStorage.value, parseFloat(tempSlider.value)],
417
+ event_data: null,
418
+ session_hash: Math.random().toString(36).substring(2)
419
  };
420
 
421
  try {
422
  const joinQueueResponse = await fetch(JOIN_QUEUE_URL, {
423
+ method: "POST",
424
+ headers: { "Content-Type": "application/json" },
425
+ body: JSON.stringify(payload)
426
  });
427
+
428
+ if (!joinQueueResponse.ok) throw new Error(`خطای سرور (${joinQueueResponse.status})`);
429
+
430
+ const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${payload.session_hash}`);
431
  const reader = dataResponse.body.getReader();
432
  const decoder = new TextDecoder();
433
  let finalFilePath = null;
 
434
 
435
  while (true) {
436
  const { value, done } = await reader.read();
437
  if (done) break;
438
+ const line = decoder.decode(value);
439
+ if (line.includes('"msg": "process_completed"')) {
440
+ try {
441
+ const data = JSON.parse(line.substring(5)); // remove "data:"
442
+ if (data.success && data.output.data[0]) {
 
 
 
443
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
 
444
  }
445
+ } catch(e) { /* ignore parse errors of partial data */ }
446
+ break;
447
  }
 
448
  }
449
 
450
  if (finalFilePath) {
451
  audioPlayer.src = `${FILE_URL_BASE}${finalFilePath}`;
452
+ audioPlayer.oncanplay = () => {
453
+ setOutputState('result');
454
+ };
455
+ } else {
456
+ throw new Error('پاسخی از سرور دریافت نشد. لطفا دوباره تلاش کنید.');
457
+ }
458
  } catch (error) {
459
+ console.error('یک خطا در فرآیند رخ داد:', error);
460
+ setOutputState('error', `یک خطا رخ داد: ${error.message}`);
461
+ } finally {
462
+ generateBtn.disabled = false;
463
+ generateBtn.textContent = '✨ خلق صدا';
464
  }
465
  }
466
 
467
+ populateSpeakerCarousel();
468
  updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
469
  form.addEventListener('submit', generateAudio);
470
  });