Hamed744 commited on
Commit
1827ca5
·
verified ·
1 Parent(s): 72b0cbe

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +502 -322
index.html CHANGED
@@ -3,319 +3,439 @@
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">
321
 
@@ -328,7 +448,7 @@
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');
@@ -337,85 +457,131 @@
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 {
@@ -425,48 +591,62 @@
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
  });
471
  </script>
472
  </body>
 
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;700;800;900&display=swap');
9
 
10
  :root {
11
+ --app-font: 'Vazirmatn', sans-serif;
12
+ --app-bg: #0D1117; /* Deep dark blue/black */
13
+ --panel-bg: #161B22; /* Slightly lighter dark */
14
+ --panel-border: #30363D;
15
+ --text-primary: #C9D1D9; /* Light gray for text */
16
+ --text-secondary: #8B949E; /* Medium gray */
17
+ --accent-primary: #58A6FF; /* Bright blue accent */
18
+ --accent-primary-hover: #79C0FF;
19
+ --accent-secondary: #A371F7; /* Purple accent */
20
+ --accent-secondary-hover: #B38FF9;
21
+
22
+ --radius-card: 16px;
23
+ --radius-input: 10px;
24
+ --shadow-strong: 0 10px 30px -10px rgba(0,0,0,0.3);
25
+ --shadow-glow-accent: 0 0 25px -5px var(--accent-primary);
26
+ --shadow-glow-secondary: 0 0 25px -5px var(--accent-secondary);
27
+ }
28
 
29
  body {
30
+ font-family: var(--app-font);
31
  direction: rtl;
32
+ background-color: var(--app-bg);
33
  color: var(--text-primary);
34
  font-size: 16px;
35
+ line-height: 1.75;
36
  margin: 0;
37
+ padding: 0;
38
  min-height: 100vh;
39
+ -webkit-font-smoothing: antialiased;
40
+ -moz-osx-font-smoothing: grayscale;
41
  display: flex;
 
42
  justify-content: center;
43
+ align-items: flex-start; /* Align to top for long content */
44
+ padding-top: 3rem;
45
+ padding-bottom: 3rem;
46
  }
47
 
48
+ .container {
49
+ max-width: 700px;
50
+ width: 90%;
51
+ margin: 0 auto;
 
 
 
 
 
 
52
  }
53
+
54
+ .app-header {
55
+ padding: 1rem 0 2rem 0;
56
+ text-align: center;
57
+ margin-bottom: 2rem;
58
  }
59
+ .app-header h1 {
60
+ font-size: 3.2em;
61
+ font-weight: 800;
62
+ margin:0 0 0.5rem 0;
63
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
64
+ -webkit-background-clip: text;
65
+ -webkit-text-fill-color: transparent;
66
+ text-shadow: 0 2px 10px rgba(0,0,0,0.2);
67
  }
68
+ .app-header p {
69
+ font-size: 1.1em;
70
+ color: var(--text-secondary);
71
+ margin-top:0;
72
+ opacity: 0.9;
73
  }
74
+
75
+ .main-content {
76
+ padding: 2.5rem;
77
+ background-color: var(--panel-bg);
78
+ border-radius: var(--radius-card);
79
+ box-shadow: 0 8px 25px rgba(0,0,0,0.2), 0 0 0 1px var(--panel-border);
80
+ border: 1px solid var(--panel-border);
81
  }
82
+
83
+ .form-group { margin-bottom: 2.5rem; }
84
+ label {
85
+ display: block;
86
+ font-weight: 600;
87
+ color: var(--text-primary);
88
+ font-size: 1.05em;
89
+ margin-bottom: 0.8rem;
90
+ }
91
+ textarea, input[type="text"] {
92
+ width: 100%;
93
+ padding: 0.9rem 1rem;
94
+ border-radius: var(--radius-input);
95
+ border: 1px solid var(--panel-border);
96
+ background-color: #0D1117; /* Darker than panel for contrast */
97
+ color: var(--text-primary);
98
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
99
+ font-family: var(--app-font);
100
+ font-size: 1rem;
101
+ box-sizing: border-box;
102
+ transition: all 0.25s ease-in-out;
103
+ }
104
+ textarea:focus, input[type="text"]:focus {
105
+ outline: none;
106
+ border-color: var(--accent-primary);
107
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 0 0 3px rgba(88, 166, 255, 0.3);
108
+ background-color: #10151D;
109
+ }
110
+ textarea { min-height: 100px; }
111
+
112
+ /* --- نمایش گوینده منتخب --- */
113
+ #selected-speaker-display { text-align: center; margin-top: 1rem; }
114
+ #selected-speaker-card {
115
+ display: inline-flex;
116
+ align-items: center;
117
+ background: linear-gradient(145deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02));
118
+ border-radius: 99px;
119
+ padding: 12px 15px 12px 25px;
120
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
121
  border: 1px solid var(--panel-border);
122
+ transition: all 0.3s ease;
123
+ }
124
+ #selected-speaker-card:hover {
125
+ transform: translateY(-3px);
126
+ box-shadow: 0 8px 20px rgba(0,0,0,0.25), var(--shadow-glow-secondary);
127
+ }
128
+ #selected-speaker-card img {
129
+ width: 70px; height: 70px;
130
+ border-radius: 50%;
131
+ object-fit: cover;
132
+ margin-left: 20px;
133
+ border: 3px solid var(--accent-secondary);
134
+ box-shadow: 0 0 15px -2px var(--accent-secondary);
135
+ background-color: #333;
136
+ }
137
+ #selected-speaker-info h3 { margin: 0; font-size: 1.3em; font-weight: 700; color: var(--text-primary); }
138
+ #selected-speaker-info p { margin: 4px 0 0; color: var(--text-secondary); font-size: 0.85em; }
139
+ #change-speaker-btn {
140
+ display: block; margin: 1rem auto 0;
141
+ padding: 10px 22px;
142
+ border-radius: var(--radius-input);
143
+ background-color: transparent;
144
+ border: 1px solid var(--accent-primary);
145
+ color: var(--accent-primary);
146
+ cursor: pointer;
147
+ font-family: var(--app-font); font-weight: 500;
148
+ transition: all 0.2s ease;
149
+ }
150
+ #change-speaker-btn:hover {
151
+ background-color: rgba(88, 166, 255, 0.1);
152
+ box-shadow: 0 0 10px -2px var(--accent-primary);
153
+ transform: translateY(-1px);
154
+ }
155
+
156
+ /* --- مودال گالری گویندگان --- */
157
+ #speaker-modal {
158
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
159
+ background-color: rgba(13, 17, 23, 0.8);
160
+ backdrop-filter: blur(10px) saturate(180%);
161
+ display: none; align-items: center; justify-content: center;
162
+ z-index: 1000; opacity: 0;
163
+ transition: opacity 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
164
+ }
165
+ #speaker-modal.visible { display: flex; opacity: 1; }
166
+ .modal-content {
167
+ background: var(--panel-bg);
168
+ padding: 2rem;
169
+ border-radius: var(--radius-card);
170
+ width: 90%; max-width: 650px;
171
+ max-height: 85vh; overflow-y: auto;
172
+ transform: scale(0.9);
173
+ transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
174
  border: 1px solid var(--panel-border);
175
+ box-shadow: 0 15px 40px rgba(0,0,0,0.4);
176
+ }
177
+ #speaker-modal.visible .modal-content { transform: scale(1); }
178
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--panel-border); }
179
+ .modal-header h2 { margin: 0; font-size: 1.5em; }
180
+ .close-modal-btn { background: none; border: none; font-size: 2.5rem; cursor: pointer; color: var(--text-secondary); transition: all 0.2s ease; line-height: 1; }
181
+ .close-modal-btn:hover { color: var(--text-primary); transform: rotate(90deg) scale(1.1); }
182
+
183
+ #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 1.2rem; }
184
+ @media (min-width: 576px) { #speaker-grid { grid-template-columns: repeat(4, 1fr); } }
185
+ .speaker-card { cursor: pointer; transition: all 0.3s ease; text-align: center; position: relative;}
186
+ .speaker-card .speaker-visual {
187
+ border: 3px solid transparent;
188
+ border-radius: var(--radius-card);
189
+ overflow: hidden;
190
+ box-shadow: 0 4px 10px rgba(0,0,0,0.2);
191
+ position: relative;
192
+ background-color: #0D1117;
193
+ transition: all 0.3s ease;
194
+ }
195
+ .speaker-card:hover .speaker-visual {
196
+ transform: translateY(-5px) scale(1.03);
197
+ box-shadow: 0 8px 20px rgba(0,0,0,0.3);
198
+ }
199
+ .speaker-card input[type="radio"] { display: none; }
200
+ .speaker-card img {
201
+ width: 100%; height: 110px;
202
+ object-fit: cover; display: block;
203
+ background-color: #333;
204
+ filter: grayscale(30%);
205
+ transition: filter 0.3s ease;
206
+ }
207
+ .speaker-card:hover img, .speaker-card input[type="radio"]:checked + .speaker-visual img {
208
+ filter: grayscale(0%);
209
+ }
210
+ .speaker-card .speaker-name { padding: 0.7rem 0.5rem; font-weight: 500; font-size: 0.9em; color: var(--text-secondary); }
211
+ .speaker-card input[type="radio"]:checked + .speaker-visual {
212
+ border-color: var(--accent-secondary);
213
+ box-shadow: 0 0 20px -4px var(--accent-secondary);
214
+ }
215
+ .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name {
216
+ color: var(--accent-secondary);
217
+ font-weight: 700;
218
+ }
219
+
220
+ /* --- Slider & Button & Output --- */
221
+ .slider-container { display: flex; align-items: center; gap: 1.2rem; }
222
+ input[type="range"] {
223
+ flex-grow: 1; -webkit-appearance: none; appearance: none;
224
+ width: 100%; height: 6px;
225
+ background: linear-gradient(90deg, var(--accent-primary) 0%, var(--accent-secondary) 100%);
226
+ border-radius: 3px; outline: none; opacity: 0.7; transition: opacity .2s;
227
  cursor: pointer;
 
 
 
 
228
  }
229
+ input[type="range"]:hover { opacity: 1; }
230
+ input[type="range"]::-webkit-slider-thumb {
231
+ -webkit-appearance: none; appearance: none;
232
+ width: 22px; height: 22px;
233
+ background: #fff;
234
+ border-radius: 50%; cursor: pointer;
235
+ border: 3px solid var(--accent-primary);
236
+ box-shadow: 0 0 8px rgba(88, 166, 255, 0.5);
237
+ transition: transform 0.2s ease;
238
  }
239
+ input[type="range"]::-webkit-slider-thumb:hover {
 
 
 
240
  transform: scale(1.1);
241
  }
242
+ input[type="range"]::-moz-range-thumb {
243
+ width: 22px; height: 22px;
244
+ background: #fff;
245
+ border-radius: 50%; cursor: pointer;
246
+ border: 3px solid var(--accent-primary);
247
+ box-shadow: 0 0 8px rgba(88, 166, 255, 0.5);
248
+ }
249
+ #temperature-value {
250
+ font-weight: bold; background-color: rgba(255,255,255,0.05);
251
+ padding: 0.3rem 0.9rem;
252
+ border-radius: 8px;
253
+ border: 1px solid var(--panel-border);
254
+ min-width: 40px; text-align: center;
255
+ color: var(--text-primary);
256
+ }
257
+
258
+ #generate-btn {
259
+ width: 100%; padding: 1.1rem 1.5rem;
260
+ font-size: 1.2em; font-weight: 700;
261
+ font-family: var(--app-font);
262
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
263
+ color: #0D1117; /* Dark text on bright button */
264
+ border: none;
265
+ border-radius: var(--radius-input);
266
+ cursor: pointer;
267
+ transition: all 0.3s ease;
268
+ box-shadow: 0 4px 15px -5px var(--accent-primary), 0 4px 15px -5px var(--accent-secondary);
269
  position: relative;
270
  overflow: hidden;
 
271
  }
272
+ #generate-btn::before {
273
+ content: '';
274
+ position: absolute;
275
+ top: 0; left: -100%;
276
+ width: 100%; height: 100%;
277
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
278
+ transition: left 0.5s ease;
279
+ }
280
+ #generate-btn:hover::before {
281
+ left: 100%;
282
+ }
283
+ #generate-btn:hover:not(:disabled) {
284
+ transform: translateY(-3px);
285
+ box-shadow: 0 6px 20px -5px var(--accent-primary), 0 6px 20px -5px var(--accent-secondary);
286
+ }
287
+ #generate-btn:disabled {
288
+ background: var(--text-secondary);
289
+ cursor: not-allowed; box-shadow: none; color: #0D1117; opacity: 0.7;
290
+ transform: none;
291
+ }
292
+ #generate-btn:disabled::before {
293
  display: none;
294
+ }
295
+
296
+ #output-section {
297
+ margin-top: 3rem;
298
+ padding: 2.5rem;
299
+ background-color: rgba(255,255,255,0.02);
300
+ border-radius: var(--radius-card);
301
+ min-height: 200px;
302
+ display: flex; align-items: center; justify-content: center;
303
+ flex-direction: column; gap: 1.5rem;
304
+ border: 1px dashed var(--panel-border);
305
+ transition: all 0.3s ease;
306
+ position: relative;
307
+ overflow: hidden;
308
+ }
309
+ #status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; }
310
+ #audio-player { width: 100%; margin-top: 1rem; display: none; filter: invert(90%) hue-rotate(180deg); } /* Custom style for dark theme */
311
+ #audio-player::-webkit-media-controls-panel { background-color: var(--panel-bg); }
312
+ #audio-player::-webkit-media-controls-play-button { color: var(--accent-primary); }
313
+
314
+
315
+ /* --- انیمیشن پردازش فوق العاده زیبا --- */
316
+ #loading-animation-wrapper {
317
+ display: none; /* Hidden by default */
318
  flex-direction: column;
319
  align-items: center;
320
  justify-content: center;
321
+ gap: 1.5rem;
322
  width: 100%;
323
+ min-height: 150px; /* Ensure it takes space */
 
 
 
 
 
 
324
  }
 
 
 
 
 
325
 
326
+ .aurora-loader {
 
327
  width: 120px;
328
  height: 120px;
 
 
329
  position: relative;
330
+ filter: drop-shadow(0 0 10px var(--accent-primary)) drop-shadow(0 0 20px var(--accent-secondary));
331
  }
332
+
333
+ .aurora-loader::before,
334
+ .aurora-loader::after {
335
+ content: "";
336
  position: absolute;
337
+ top: 50%;
338
+ left: 50%;
339
+ width: 100%;
340
+ height: 100%;
341
  border-radius: 50%;
342
+ background: radial-gradient(circle, transparent 30%, var(--accent-primary) 50%, var(--accent-secondary) 70%, transparent 90%);
343
+ opacity: 0.7;
344
+ transform-origin: center center;
345
+ animation: aurora-spin 4s linear infinite, aurora-pulse 3s ease-in-out infinite alternate;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
 
348
+ .aurora-loader::after {
349
+ width: 80%;
350
+ height: 80%;
351
+ background: radial-gradient(circle, transparent 20%, var(--accent-secondary) 40%, var(--accent-primary) 60%, transparent 80%);
352
+ animation-delay: -2s; /* Offset animation */
353
+ opacity: 0.5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
+
356
+ @keyframes aurora-spin {
357
+ 0% { transform: translate(-50%, -50%) rotate(0deg) scale(0.8); }
358
+ 50% { transform: translate(-50%, -50%) rotate(180deg) scale(1); }
359
+ 100% { transform: translate(-50%, -50%) rotate(360deg) scale(0.8); }
360
+ }
361
+ @keyframes aurora-pulse {
362
+ 0% { filter: brightness(0.8) blur(2px); }
363
+ 100% { filter: brightness(1.2) blur(0px); }
364
+ }
365
+
366
+ #loading-text {
367
+ font-size: 1.15em;
368
+ font-weight: 500;
369
+ color: var(--text-primary);
370
+ text-align: center;
371
+ text-shadow: 0 0 5px rgba(255,255,255,0.2);
372
+ }
373
+
374
  </style>
375
  </head>
376
  <body>
 
377
  <div class="container">
378
  <header class="app-header">
379
+ <h1>آلفا TTS</h1>
380
+ <p>جادوی تبدیل متن به صدا، با کیفیتی بی‌نظیر برای شما</p>
381
  </header>
382
 
383
+ <main class="main-content">
384
+ <form id="tts-form">
385
+ <div class="form-group">
386
+ <label for="text-input">📝 متن برای تبدیل</label>
387
+ <textarea id="text-input" rows="4" placeholder="متن خود را با دقت اینجا وارد نمایید...">این یک آزمایش پیشرفته برای بررسی عمق و پویایی صدای تولید شده توسط نسل جدید هوش مصنوعی آلفا است، با تمرکز بر وضوح و طبیعی بودن گفتار.</textarea>
388
+ </div>
389
+ <div class="form-group">
390
+ <label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری، برای نتایج حرفه‌ای)</label>
391
+ <input type="text" id="prompt-input" value="با صدایی گیرا، آرام و متقاعدکننده." placeholder="مثال: با لحنی حماسی و پرشور، یا دوستانه و صمیمی">
392
+ </div>
393
+
394
+ <div class="form-group">
395
+ <label>🎤 گوینده منتخب شما</label>
396
+ <div id="selected-speaker-display">
397
+ <div id="selected-speaker-card">
398
+ <img id="selected-speaker-img" src="" alt="عکس گوینده">
399
+ <div id="selected-speaker-info">
400
+ <h3 id="selected-speaker-name"></h3>
401
+ <p>برای انتخاب گوینده، کلیک کنید</p>
402
+ </div>
 
 
 
 
 
403
  </div>
404
+ <button type="button" id="change-speaker-btn">تغییر گوینده</button>
405
  </div>
406
+ </div>
 
 
 
 
 
407
 
408
+ <div class="form-group">
409
+ <label for="temperature-slider">🌡️ میزان خلاقیت و پویایی صدا (0.1 تا 1.5)</label>
410
+ <div class="slider-container">
411
+ <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
412
+ <span id="temperature-value">0.9</span>
 
413
  </div>
414
+ </div>
415
 
416
+ <button type="submit" id="generate-btn">✨ خلق صدا با هوش مصنوعی آلفا</button>
417
+ </form>
 
 
 
 
 
 
 
 
418
 
419
+ <div id="output-section">
420
+ <div id="status-message">خروجی صدای شما در اینجا ظاهر خواهد شد.</div>
421
+ <div id="loading-animation-wrapper">
422
+ <div class="aurora-loader"></div>
423
+ <p id="loading-text">در حال خلق اثر صوتی شما با هوش مصنوعی آلفا...</p>
 
 
 
 
 
424
  </div>
425
+ <audio id="audio-player" controls></audio>
426
  </div>
 
 
427
  </main>
428
  </div>
429
+
430
+ <div id="speaker-modal">
431
+ <div class="modal-content">
432
+ <div class="modal-header">
433
+ <h2>انتخاب استاد صدا</h2>
434
+ <button type="button" class="close-modal-btn">×</button>
435
+ </div>
436
+ <div id="speaker-grid"></div>
437
+ </div>
438
+ </div>
439
 
440
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
441
 
 
448
  const FN_INDEX = 1;
449
 
450
  const speakers = [
451
+ { 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: "آناهیتا (زن)" }, { id: "Despina", name: "دلنواز (زن)" }, { id: "Erinome", name: "رسا (مرد)" }, { id: "Algenib", name: "امید (مرد)" }, { id: "Rasalthgeti", name: "الهه (زن)" }, { id: "Orus", name: "بردیا (مرد)" }, { id: "Aoede", name: "ترانه (زن)" }, { id: "Callirrhoe", name: "نیما (مرد)" }, { id: "Autonoe", name: "هستی (زن)" }, { id: "Enceladus", name: "کامیار (مرد)" }, { id: "Iapetus", name: "ستاره (زن)" }, { id: "Puck", name: "پویا (مرد)" }, { id: "Kore", name: "مهتاب (زن)" }, { id: "Fenrir", name: "سام (مرد)" }, { id: "Leda", name: "لیدا (زن)" }
452
  ];
453
 
454
  const form = document.getElementById('tts-form');
 
457
  const tempSlider = document.getElementById('temperature-slider');
458
  const tempValueSpan = document.getElementById('temperature-value');
459
  const generateBtn = document.getElementById('generate-btn');
 
460
 
461
+ const statusMessage = document.getElementById('status-message');
 
 
 
 
 
462
  const audioPlayer = document.getElementById('audio-player');
463
+ const loadingAnimationWrapper = document.getElementById('loading-animation-wrapper');
464
+
465
+ const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
466
+ const speakerModal = document.getElementById('speaker-modal');
467
+ const changeSpeakerBtn = document.getElementById('change-speaker-btn');
468
+ const closeModalBtn = document.querySelector('.close-modal-btn');
469
+ const speakerGridInModal = document.getElementById('speaker-grid');
470
+ const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
471
+ const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
472
+
473
+ function getSpeakerById(id) {
474
+ return speakers.find(s => s.id === id);
475
  }
476
 
477
+ function getImageUrl(speaker, index, size = 'medium') { // 'thumb', 'medium', 'large'
478
+ const gender = speaker.name.includes('(مرد)') ? 'men' : (speaker.name.includes('(زن)') ? 'women' : 'lego');
479
+ const imageIndex = (index * 11 + 23) % 100; // Slightly different formula for variety
480
+ // randomuser.me API provides different sizes. For "thumb" in modal, 'large' for selected.
481
+ let portraitSize = 'thumb'; // default for modal
482
+ if (size === 'large') portraitSize = ''; // API uses no path for largest, just /men/idx.jpg
483
+
484
+ return `https://randomuser.me/api/portraits/${portraitSize ? portraitSize+'/' : ''}${gender}/${imageIndex}.jpg`;
485
+ }
486
+
487
  function updateSelectedSpeakerDisplay(speakerId) {
488
+ const speaker = getSpeakerById(speakerId);
 
489
  if (speaker) {
490
+ const speakerIndex = speakers.findIndex(s => s.id === speakerId);
491
+ selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex, 'large');
492
+ selectedSpeakerNameDisplay.textContent = speaker.name;
493
  selectedSpeakerIdStorage.value = speaker.id;
 
 
 
 
 
 
 
 
494
  }
495
  }
496
 
497
+ function createSpeakerCardsInModal() {
498
+ speakerGridInModal.innerHTML = '';
499
  speakers.forEach((speaker, index) => {
500
+ const card = document.createElement('label');
501
+ card.className = 'speaker-card';
502
+ card.setAttribute('for', `modal-speaker-${speaker.id}`);
503
+ const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
504
+
505
+ card.innerHTML = `
506
+ <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
507
+ <div class="speaker-visual">
508
+ <img src="${getImageUrl(speaker, index, 'thumb')}" alt="${speaker.name}" loading="lazy">
509
+ <div class="speaker-name">${speaker.name}</div>
510
+ </div>
511
  `;
512
+
513
+ card.addEventListener('click', () => {
514
  updateSelectedSpeakerDisplay(speaker.id);
515
+ setTimeout(() => speakerModal.classList.remove('visible'), 250); // Slightly longer for smoother feel
516
  });
517
+
518
+ speakerGridInModal.appendChild(card);
519
  });
520
  }
521
+
522
+ changeSpeakerBtn.addEventListener('click', () => {
523
+ createSpeakerCardsInModal();
524
+ speakerModal.classList.add('visible');
525
+ });
526
+ // Also allow clicking the selected speaker card to open modal
527
+ document.getElementById('selected-speaker-card').addEventListener('click', () => {
528
+ createSpeakerCardsInModal();
529
+ speakerModal.classList.add('visible');
530
+ });
531
 
532
+ closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
533
+ speakerModal.addEventListener('click', (e) => {
534
+ if (e.target === speakerModal) { // Click on backdrop
535
+ speakerModal.classList.remove('visible');
536
  }
537
+ });
538
 
539
  tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
540
 
541
+ function showLoadingState() {
542
+ statusMessage.style.display = 'none';
543
+ audioPlayer.style.display = 'none';
544
+ audioPlayer.src = '';
545
+ loadingAnimationWrapper.style.display = 'flex';
546
+ generateBtn.disabled = true;
547
+ generateBtn.textContent = 'در حال خلق...';
548
+ }
549
+
550
+ function showResultState(isSuccess, message = '') {
551
+ loadingAnimationWrapper.style.display = 'none';
552
+ if (isSuccess) {
553
+ statusMessage.style.display = 'none';
554
+ audioPlayer.style.display = 'block';
555
+ // audioPlayer.play(); // Autoplay can be annoying, let user click
556
+ } else {
557
+ statusMessage.textContent = message || 'یک خطای غیرمنتظره رخ داد. لطفاً مجدداً تلاش کنید.';
558
+ statusMessage.style.display = 'block';
559
+ audioPlayer.style.display = 'none';
560
+ }
561
+ generateBtn.disabled = false;
562
+ generateBtn.textContent = '✨ خلق صدا با هوش مصنوعی آلفا';
563
+ }
564
+
565
  async function generateAudio(event) {
566
  event.preventDefault();
567
+ showLoadingState();
 
 
568
 
569
  const text = textInput.value;
570
  if (!text.trim()) {
571
+ showResultState(false, 'خطا: متن ورودی نمی‌تواند خالی باشد.');
 
 
572
  return;
573
  }
574
 
575
+ const prompt = promptInput.value;
576
+ const temperature = parseFloat(tempSlider.value);
577
+ const selectedSpeaker = selectedSpeakerIdStorage.value;
578
+ const sessionHash = Math.random().toString(36).substring(2);
579
+
580
  const payload = {
581
  fn_index: FN_INDEX,
582
+ data: [false, null, text, prompt, selectedSpeaker, temperature],
583
  event_data: null,
584
+ session_hash: sessionHash
585
  };
586
 
587
  try {
 
591
  body: JSON.stringify(payload)
592
  });
593
 
594
+ if (!joinQueueResponse.ok) {
595
+ const errorBody = await joinQueueResponse.text();
596
+ throw new Error(`خطا در برقراری ارتباط با سرویس (${joinQueueResponse.status}).`);
597
+ }
598
+
599
+ const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
600
  const reader = dataResponse.body.getReader();
601
  const decoder = new TextDecoder();
602
  let finalFilePath = null;
603
+ let buffer = '';
604
 
605
  while (true) {
606
  const { value, done } = await reader.read();
607
  if (done) break;
608
+ buffer += decoder.decode(value, { stream: true });
609
+ const lines = buffer.split('\n');
610
+ buffer = lines.pop();
611
+ for (const line of lines) {
612
+ if (!line.startsWith('data:')) continue;
613
+ try {
614
+ const data = JSON.parse(line.substring(5));
615
+ if (data.msg === 'process_generating') {
616
+ // You could potentially update a progress bar here if the API provided steps
617
+ }
618
+ if (data.msg === 'process_completed') {
619
+ if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
620
+ finalFilePath = data.output.data[0].name || data.output.data[0].path;
621
+ } else { console.error("ساختار داده پاسخ سرور معتبر نیست:", data); }
622
+ break;
623
  }
624
+ } catch (e) { /* Ignore JSON parse errors on partial stream data */ }
 
625
  }
626
+ if (finalFilePath) break;
627
  }
628
 
629
  if (finalFilePath) {
630
+ const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
631
+ audioPlayer.src = audioUrl;
632
+ showResultState(true);
 
633
  } else {
634
+ throw new Error('فایل صوتی از سرور دریافت نشد. ممکن است پردازش با مشکل مواجه شده باشد.');
635
  }
636
+
637
  } catch (error) {
638
+ console.error('خطا در فرآیند تولید صدا:', error);
639
+ showResultState(false, `خطا: ${error.message}`);
 
 
 
640
  }
641
  }
642
 
643
+ updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value); // Load default speaker
 
644
  form.addEventListener('submit', generateAudio);
645
+
646
+ // Initial state for output section
647
+ statusMessage.style.display = 'block';
648
+ loadingAnimationWrapper.style.display = 'none';
649
+ audioPlayer.style.display = 'none';
650
  });
651
  </script>
652
  </body>