Hamed744 commited on
Commit
7b3c065
·
verified ·
1 Parent(s): 1b9837b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +750 -137
index.html CHANGED
@@ -9,21 +9,27 @@
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
- --app-header-grad-start: #1e3a8a;
13
- --app-header-grad-end: #3b82f6;
14
- --app-panel-bg: #FFFFFF;
15
- --app-input-bg: #f7f8fc;
16
- --app-button-bg: #6D28D9;
17
- --app-button-hover-bg: #5B21B6;
18
- --app-main-bg: #f0f4f9;
19
- --app-text-primary: #1f2937;
20
- --app-text-secondary: #6b7280;
21
- --app-border-color: #e5e7eb;
22
  --radius-card: 24px;
23
- --radius-input: 12px;
24
- --shadow-card: 0 10px 30px -5px rgba(30, 58, 138, 0.1);
25
- --shadow-button: 0 4px 15px -2px rgba(109, 40, 217, 0.4);
26
- --speaker-selected-glow: 0 0 20px rgba(109, 40, 217, 0.4);
 
 
 
 
 
 
27
  }
28
 
29
  body {
@@ -32,101 +38,653 @@
32
  background: var(--app-main-bg);
33
  color: var(--app-text-primary);
34
  font-size: 16px;
35
- line-height: 1.7;
36
  margin: 0;
37
  padding: 0;
38
  min-height: 100vh;
39
  -webkit-font-smoothing: antialiased;
40
  -moz-osx-font-smoothing: grayscale;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
- .container { max-width: 800px; width: 95%; margin: 0 auto; padding-bottom: 40px; }
44
- .app-header { padding: 3rem 1.5rem 6rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(30, 58, 138, 0.25); }
45
- .app-header h1 { font-size: 3em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 5px rgba(0,0,0,0.2); }
46
- .app-header p { font-size: 1.25em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.95; }
47
- .main-content { padding: 2.5rem; margin: -4.5rem auto 2rem auto; background-color: var(--app-panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-card); }
48
- .form-group { margin-bottom: 2.5rem; }
49
- label { display: block; font-weight: 700; color: var(--app-text-primary); font-size: 1.1em; margin-bottom: 1rem; }
50
- textarea, input[type="text"] { width: 100%; padding: 1rem; border-radius: var(--radius-input); border: 2px solid var(--app-border-color); background-color: var(--app-input-bg); box-shadow: none; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: all 0.25s ease-in-out; }
51
- textarea:focus, input[type="text"]:focus { outline: none; border-color: var(--app-button-bg); box-shadow: 0 0 0 4px rgba(109, 40, 217, 0.15); background-color: #fff; }
52
-
53
- /* --- نمایش گوینده منتخب --- */
54
- #selected-speaker-display { text-align: center; }
55
- #selected-speaker-card { display: inline-flex; align-items: center; background: linear-gradient(145deg, #ffffff, #f7f8fc); border-radius: 99px; padding: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.07); border: 1px solid var(--app-border-color); }
56
- #selected-speaker-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-left: 20px; border: 3px solid #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background-color: #eee; }
57
- #selected-speaker-info h3 { margin: 0; font-size: 1.5em; font-weight: 700; }
58
- #selected-speaker-info p { margin: 5px 0 0; color: var(--app-text-secondary); font-size: 0.9em; }
59
- #change-speaker-btn { display: block; margin: 1.2rem auto 0; padding: 10px 24px; border-radius: var(--radius-input); background-color: var(--app-input-bg); border: 1px solid var(--app-border-color); cursor: pointer; font-family: var(--app-font); font-weight: 500; transition: all 0.2s ease; }
60
- #change-speaker-btn:hover { background-color: #e9ecef; border-color: #ced4da; transform: translateY(-1px); }
61
-
62
- /* --- مودال گالری گویندگان --- */
63
- #speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(17, 24, 39, 0.6); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; }
64
- #speaker-modal.visible { display: flex; opacity: 1; }
65
- .modal-content { background: #fff; padding: 2rem; border-radius: var(--radius-card); width: 90%; max-width: 700px; max-height: 85vh; overflow-y: auto; transform: scale(0.95); transition: transform 0.3s ease; }
66
- #speaker-modal.visible .modal-content { transform: scale(1); }
67
- .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--app-border-color); }
68
- .modal-header h2 { margin: 0; }
69
- .close-modal-btn { background: none; border: none; font-size: 2.2rem; cursor: pointer; color: #9ca3af; transition: color 0.2s ease, transform 0.2s ease; line-height: 1; }
70
- .close-modal-btn:hover { color: var(--app-text-primary); transform: rotate(90deg); }
71
- #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.5rem; }
72
- @media (min-width: 576px) { #speaker-grid { grid-template-columns: repeat(4, 1fr); } }
73
- .speaker-card { cursor: pointer; transition: all 0.3s ease; text-align: center; }
74
- .speaker-card .speaker-visual { border: 4px solid transparent; border-radius: var(--radius-card); overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.05); position: relative; background-color: #fff; transition: all 0.3s ease; }
75
- .speaker-card:hover .speaker-visual { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
76
- .speaker-card input[type="radio"] { display: none; }
77
- .speaker-card img { width: 100%; height: 120px; object-fit: cover; display: block; background-color: #eee; }
78
- .speaker-card .speaker-name { padding: 0.8rem 0.5rem; font-weight: 500; font-size: 0.95em; color: var(--app-text-secondary); transition: color 0.2s; }
79
- .speaker-card:hover .speaker-name { color: var(--app-text-primary); }
80
- .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--app-button-bg); box-shadow: var(--speaker-selected-glow); }
81
- .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name { color: var(--app-button-bg); font-weight: 700; }
82
-
83
- /* --- Slider & Button & Output --- */
84
- .slider-container { display: flex; align-items: center; gap: 1.5rem; }
85
- input[type="range"] { flex-grow: 1; -webkit-appearance: none; appearance: none; width: 100%; height: 8px; background: #e5e7eb; border-radius: 5px; outline: none; transition: background 0.2s; }
86
- input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: var(--app-button-bg); border-radius: 50%; cursor: pointer; border: 4px solid #fff; box-shadow: 0 0 5px rgba(0,0,0,0.2); }
87
- input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: var(--app-button-bg); border-radius: 50%; cursor: pointer; border: 4px solid #fff; box-shadow: 0 0 5px rgba(0,0,0,0.2); }
88
- #temperature-value { font-weight: 700; background-color: var(--app-input-bg); padding: 0.4rem 1rem; border-radius: 8px; border: 1px solid var(--app-border-color); min-width: 45px; text-align: center; }
89
- #generate-btn { width: 100%; padding: 1rem 1.5rem; font-size: 1.25em; font-weight: 700; font-family: var(--app-font); background: var(--app-button-bg); color: white; border: none; border-radius: var(--radius-input); cursor: pointer; transition: all 0.3s ease; box-shadow: var(--shadow-button); }
90
- #generate-btn:hover:not(:disabled) { background-color: var(--app-button-hover-bg); transform: translateY(-3px); box-shadow: 0 6px 20px -3px rgba(109, 40, 217, 0.5); }
91
- #generate-btn:disabled { background-color: #9ca3af; cursor: not-allowed; box-shadow: none; transform: none; }
92
- #output-section { margin-top: 2.5rem; padding: 2rem; background-color: #fff; border-radius: var(--radius-card); min-height: 150px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; border: 1px dashed var(--app-border-color); transition: all 0.3s ease; }
93
- #status-message { font-weight: 500; color: var(--app-text-secondary); text-align: center; }
94
- #audio-player { width: 100%; margin-top: 1rem; display: none; }
95
-
96
- /* --- انیمیشن پردازش --- */
97
- #loading-animation { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 1.5rem; }
98
- .sound-wave-svg { width: 100px; height: 100px; }
99
- .sound-wave-svg path { stroke: var(--app-button-bg); stroke-width: 3; fill: none; stroke-linecap: round; }
100
- .wave-1 { animation: pulse 2s infinite cubic-bezier(0.55, 0.055, 0.675, 0.19); }
101
- .wave-2 { animation: pulse 2s infinite cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-delay: 0.2s; }
102
- .wave-3 { animation: pulse 2s infinite cubic-bezier(0.55, 0.055, 0.675, 0.19); animation-delay: 0.4s; }
103
- #loading-text { font-size: 1.1em; font-weight: 500; color: var(--app-text-primary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  @keyframes pulse {
105
- 0% { transform: scale(0.5); opacity: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  50% { opacity: 1; }
107
- 100% { transform: scale(1.2); opacity: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
  </style>
110
  </head>
111
  <body>
112
  <div class="container">
113
  <header class="app-header">
114
- <h1>آلفا TTS</h1>
115
- <p>جادوی تبدیل متن به صدا، به سبک شما</p>
116
  </header>
117
 
118
  <main class="main-content">
119
  <form id="tts-form">
120
  <div class="form-group">
121
- <label for="text-input">📝 متن برای تبدیل</label>
122
  <textarea id="text-input" rows="5" placeholder="اینجا متن خود را به فارسی وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
123
  </div>
 
124
  <div class="form-group">
125
- <label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری)</label>
126
  <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی">
127
  </div>
 
128
  <div class="form-group">
129
- <label>🎤 گوینده منتخب</label>
130
  <div id="selected-speaker-display">
131
  <div id="selected-speaker-card">
132
  <img id="selected-speaker-img" src="" alt="عکس گوینده">
@@ -135,45 +693,53 @@
135
  <p>برای ��غییر، روی دکمه زیر کلیک کنید</p>
136
  </div>
137
  </div>
138
- <button type="button" id="change-speaker-btn">تغییر گوینده</button>
139
  </div>
140
  </div>
 
141
  <div class="form-group">
142
- <label for="temperature-slider">🌡️ میزان خلاقیت صدا (0.1 تا 1.5)</label>
143
  <div class="slider-container">
144
  <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
145
  <span id="temperature-value">0.9</span>
146
  </div>
147
  </div>
 
148
  <button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button>
149
  </form>
150
 
151
  <div id="output-section">
152
- <div id="status-message">خروجی صدا در اینجا نمایش داده می‌شود</div>
153
- <!-- انیمیشن لودینگ در اینجا قرار می‌گیرد -->
154
- <div id="loading-animation">
155
- <svg class="sound-wave-svg" viewBox="0 0 100 100">
156
- <path class="wave-1" d="M 20 50 Q 35 20 50 50 T 80 50"></path>
157
- <path class="wave-2" d="M 20 50 Q 35 20 50 50 T 80 50"></path>
158
- <path class="wave-3" d="M 20 50 Q 35 20 50 50 T 80 50"></path>
159
- </svg>
160
- <p id="loading-text">در حال تبدیل متن به صدا با هوش مصنوعی آلفا...</p>
161
- </div>
162
  <audio id="audio-player" controls></audio>
163
  </div>
164
  </main>
165
  </div>
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  <div id="speaker-modal">
168
  <div class="modal-content">
169
  <div class="modal-header">
170
- <h2>انتخاب گوینده</h2>
171
  <button type="button" class="close-modal-btn">×</button>
172
  </div>
173
  <div id="speaker-grid"></div>
174
  </div>
175
  </div>
176
-
177
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
178
 
179
  <script>
@@ -194,11 +760,9 @@
194
  const tempSlider = document.getElementById('temperature-slider');
195
  const tempValueSpan = document.getElementById('temperature-value');
196
  const generateBtn = document.getElementById('generate-btn');
197
-
198
- // Output section elements
199
  const statusMessage = document.getElementById('status-message');
200
  const audioPlayer = document.getElementById('audio-player');
201
- const loadingAnimation = document.getElementById('loading-animation');
202
 
203
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
204
  const speakerModal = document.getElementById('speaker-modal');
@@ -215,14 +779,14 @@
215
  function getImageUrl(speaker, index) {
216
  const gender = speaker.name.includes('(مرد)') ? 'men' : (speaker.name.includes('(زن)') ? 'women' : 'lego');
217
  const imageIndex = (index * 7 + 13) % 100;
218
- return `https://randomuser.me/api/portraits/thumb/${gender}/${imageIndex}.jpg`;
219
  }
220
 
221
  function updateSelectedSpeakerDisplay(speakerId) {
222
  const speaker = getSpeakerById(speakerId);
223
  if (speaker) {
224
  const speakerIndex = speakers.findIndex(s => s.id === speakerId);
225
- selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex).replace('thumb/', ''); // Get larger image
226
  selectedSpeakerNameDisplay.textContent = speaker.name;
227
  selectedSpeakerIdStorage.value = speaker.id;
228
  }
@@ -234,6 +798,7 @@
234
  const card = document.createElement('label');
235
  card.className = 'speaker-card';
236
  card.setAttribute('for', `modal-speaker-${speaker.id}`);
 
237
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
238
 
239
  card.innerHTML = `
@@ -253,58 +818,60 @@
253
  });
254
  }
255
 
 
 
 
 
 
 
 
 
 
 
 
256
  changeSpeakerBtn.addEventListener('click', () => {
257
  createSpeakerCardsInModal();
258
  speakerModal.classList.add('visible');
259
  });
 
260
  closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
 
261
  speakerModal.addEventListener('click', (e) => {
262
  if (e.target === speakerModal) {
263
  speakerModal.classList.remove('visible');
264
  }
265
  });
266
 
267
- tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
268
-
269
- function showLoadingState() {
270
- statusMessage.style.display = 'none';
271
- audioPlayer.style.display = 'none';
272
- audioPlayer.src = '';
273
- loadingAnimation.style.display = 'flex';
274
- generateBtn.disabled = true;
275
- generateBtn.textContent = 'کمی صبر کنید...';
276
- }
277
-
278
- function showResultState(isSuccess, message = '') {
279
- loadingAnimation.style.display = 'none';
280
- if (isSuccess) {
281
- statusMessage.style.display = 'none';
282
- audioPlayer.style.display = 'block';
283
- audioPlayer.play();
284
- } else {
285
- statusMessage.textContent = message || 'یک خطای ناشناخته رخ داد.';
286
- statusMessage.style.display = 'block';
287
- audioPlayer.style.display = 'none';
288
- }
289
- generateBtn.disabled = false;
290
- generateBtn.textContent = '🚀 تولید و پخش صدا';
291
- }
292
 
293
  async function generateAudio(event) {
294
  event.preventDefault();
295
- showLoadingState();
 
 
 
 
 
 
 
 
296
 
297
  const text = textInput.value;
298
- if (!text.trim()) {
299
- showResultState(false, 'خطا: متن ورودی نمی‌تواند خالی باشد.');
300
- return;
301
- }
302
-
303
  const prompt = promptInput.value;
304
  const temperature = parseFloat(tempSlider.value);
305
  const selectedSpeaker = selectedSpeakerIdStorage.value;
306
  const sessionHash = Math.random().toString(36).substring(2);
307
 
 
 
 
 
 
 
 
 
308
  const payload = {
309
  fn_index: FN_INDEX,
310
  data: [false, null, text, prompt, selectedSpeaker, temperature],
@@ -324,6 +891,8 @@
324
  throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status}): ${errorBody}`);
325
  }
326
 
 
 
327
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
328
  const reader = dataResponse.body.getReader();
329
  const decoder = new TextDecoder();
@@ -333,17 +902,25 @@
333
  while (true) {
334
  const { value, done } = await reader.read();
335
  if (done) break;
 
336
  buffer += decoder.decode(value, { stream: true });
337
  const lines = buffer.split('\n');
338
  buffer = lines.pop();
 
339
  for (const line of lines) {
340
  if (!line.startsWith('data:')) continue;
 
341
  try {
342
  const data = JSON.parse(line.substring(5));
 
 
 
343
  if (data.msg === 'process_completed') {
344
  if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
345
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
346
- } else { console.error("ساختار پیام موفقیت مورد انتظار نبود:", data); }
 
 
347
  break;
348
  }
349
  } catch (e) { /* نادیده گرفتن خطاهای پارس */ }
@@ -352,21 +929,57 @@
352
  }
353
 
354
  if (finalFilePath) {
 
355
  const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
356
  audioPlayer.src = audioUrl;
357
- showResultState(true);
 
 
 
 
 
 
 
 
 
 
358
  } else {
359
- throw new Error('فایل صوتی از سرور دریافت نشد.');
360
  }
361
 
362
  } catch (error) {
363
  console.error('یک خطا در فرآیند رخ داد:', error);
364
- showResultState(false, `یک خطا رخ داد: ${error.message}`);
 
 
 
 
365
  }
366
  }
367
 
 
368
  updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
369
  form.addEventListener('submit', generateAudio);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  });
371
  </script>
372
  </body>
 
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
+ --app-header-grad-start: #667eea;
13
+ --app-header-grad-end: #764ba2;
14
+ --app-panel-bg: rgba(255, 255, 255, 0.95);
15
+ --app-input-bg: rgba(248, 249, 250, 0.8);
16
+ --app-button-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ --app-button-hover-bg: linear-gradient(135deg, #5a6fd8 0%, #654396 100%);
18
+ --app-main-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+ --app-text-primary: #2c3e50;
20
+ --app-text-secondary: #6c757d;
21
+ --app-border-color: rgba(224, 224, 224, 0.5);
22
  --radius-card: 24px;
23
+ --radius-input: 16px;
24
+ --shadow-card: 0 20px 40px -10px rgba(0,0,0,0.15);
25
+ --shadow-button: 0 8px 25px -5px rgba(102, 126, 234, 0.4);
26
+ --speaker-selected-glow: 0 0 20px rgba(102, 126, 234, 0.6);
27
+ --animation-primary: #667eea;
28
+ --animation-secondary: #764ba2;
29
+ }
30
+
31
+ * {
32
+ box-sizing: border-box;
33
  }
34
 
35
  body {
 
38
  background: var(--app-main-bg);
39
  color: var(--app-text-primary);
40
  font-size: 16px;
41
+ line-height: 1.65;
42
  margin: 0;
43
  padding: 0;
44
  min-height: 100vh;
45
  -webkit-font-smoothing: antialiased;
46
  -moz-osx-font-smoothing: grayscale;
47
+ overflow-x: hidden;
48
+ }
49
+
50
+ /* Animated Background */
51
+ body::before {
52
+ content: '';
53
+ position: fixed;
54
+ top: 0;
55
+ left: 0;
56
+ width: 100%;
57
+ height: 100%;
58
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
59
+ z-index: -2;
60
+ }
61
+
62
+ body::after {
63
+ content: '';
64
+ position: fixed;
65
+ top: 0;
66
+ left: 0;
67
+ width: 100%;
68
+ height: 100%;
69
+ background-image:
70
+ radial-gradient(circle at 20% 50%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
71
+ radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 0%, transparent 50%),
72
+ radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
73
+ animation: float 20s ease-in-out infinite;
74
+ z-index: -1;
75
+ }
76
+
77
+ @keyframes float {
78
+ 0%, 100% { transform: translateY(0px) rotate(0deg); }
79
+ 50% { transform: translateY(-20px) rotate(10deg); }
80
+ }
81
+
82
+ .container {
83
+ max-width: 900px;
84
+ width: 95%;
85
+ margin: 0 auto;
86
+ padding-bottom: 40px;
87
+ }
88
+
89
+ .app-header {
90
+ padding: 4rem 2rem 6rem 2rem;
91
+ text-align: center;
92
+ color: white;
93
+ position: relative;
94
+ overflow: hidden;
95
+ }
96
+
97
+ .app-header::before {
98
+ content: '';
99
+ position: absolute;
100
+ top: -50%;
101
+ left: -50%;
102
+ width: 200%;
103
+ height: 200%;
104
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
105
+ animation: rotate 30s linear infinite;
106
+ }
107
+
108
+ @keyframes rotate {
109
+ from { transform: rotate(0deg); }
110
+ to { transform: rotate(360deg); }
111
+ }
112
+
113
+ .app-header h1 {
114
+ font-size: 3.5em;
115
+ font-weight: 800;
116
+ margin: 0 0 1rem 0;
117
+ text-shadow: 0 4px 8px rgba(0,0,0,0.3);
118
+ position: relative;
119
+ z-index: 1;
120
+ background: linear-gradient(45deg, #fff, #f8f9ff);
121
+ -webkit-background-clip: text;
122
+ -webkit-text-fill-color: transparent;
123
+ background-clip: text;
124
+ }
125
+
126
+ .app-header p {
127
+ font-size: 1.4em;
128
+ color: rgba(255,255,255,0.95);
129
+ margin-top: 0;
130
+ position: relative;
131
+ z-index: 1;
132
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
133
+ }
134
+
135
+ .main-content {
136
+ padding: 3rem;
137
+ margin: -4rem auto 2rem auto;
138
+ background: var(--app-panel-bg);
139
+ backdrop-filter: blur(20px);
140
+ border-radius: var(--radius-card);
141
+ box-shadow: var(--shadow-card);
142
+ border: 1px solid rgba(255, 255, 255, 0.2);
143
+ position: relative;
144
+ overflow: hidden;
145
+ }
146
+
147
+ .main-content::before {
148
+ content: '';
149
+ position: absolute;
150
+ top: 0;
151
+ left: 0;
152
+ right: 0;
153
+ height: 1px;
154
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
155
+ }
156
+
157
+ .form-group {
158
+ margin-bottom: 3rem;
159
+ position: relative;
160
+ }
161
+
162
+ label {
163
+ display: block;
164
+ font-weight: 700;
165
+ color: var(--app-text-primary);
166
+ font-size: 1.2em;
167
+ margin-bottom: 1rem;
168
+ position: relative;
169
+ padding-right: 2rem;
170
+ }
171
+
172
+ label::before {
173
+ content: attr(data-icon);
174
+ position: absolute;
175
+ right: 0;
176
+ top: 0;
177
+ font-size: 1.2em;
178
+ }
179
+
180
+ textarea, input[type="text"] {
181
+ width: 100%;
182
+ padding: 1.5rem;
183
+ border-radius: var(--radius-input);
184
+ border: 2px solid var(--app-border-color);
185
+ background: var(--app-input-bg);
186
+ backdrop-filter: blur(10px);
187
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
188
+ font-family: var(--app-font);
189
+ font-size: 1.1rem;
190
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
191
+ resize: vertical;
192
+ }
193
+
194
+ textarea:focus, input[type="text"]:focus {
195
+ outline: none;
196
+ border-color: var(--animation-primary);
197
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.2);
198
+ background: rgba(255, 255, 255, 0.9);
199
+ transform: translateY(-2px);
200
+ }
201
+
202
+ /* Speaker Selection Styles */
203
+ #selected-speaker-display {
204
+ text-align: center;
205
+ }
206
+
207
+ #selected-speaker-card {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ background: rgba(255, 255, 255, 0.9);
211
+ backdrop-filter: blur(20px);
212
+ border-radius: 50px;
213
+ padding: 15px;
214
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
215
+ border: 2px solid rgba(255, 255, 255, 0.3);
216
+ transition: all 0.3s ease;
217
+ }
218
+
219
+ #selected-speaker-card:hover {
220
+ transform: translateY(-3px);
221
+ box-shadow: 0 15px 40px rgba(0,0,0,0.2);
222
+ }
223
+
224
+ #selected-speaker-card img {
225
+ width: 90px;
226
+ height: 90px;
227
+ border-radius: 50%;
228
+ object-fit: cover;
229
+ margin-left: 20px;
230
+ border: 3px solid rgba(255, 255, 255, 0.8);
231
+ box-shadow: 0 4px 15px rgba(0,0,0,0.2);
232
+ }
233
+
234
+ #selected-speaker-info h3 {
235
+ margin: 0;
236
+ font-size: 1.5em;
237
+ font-weight: 700;
238
+ }
239
+
240
+ #selected-speaker-info p {
241
+ margin: 8px 0 0;
242
+ color: var(--app-text-secondary);
243
+ font-size: 1rem;
244
+ }
245
+
246
+ #change-speaker-btn {
247
+ display: block;
248
+ margin: 1.5rem auto 0;
249
+ padding: 12px 30px;
250
+ border-radius: 25px;
251
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
252
+ border: 2px solid rgba(102, 126, 234, 0.3);
253
+ cursor: pointer;
254
+ font-family: var(--app-font);
255
+ font-weight: 600;
256
+ font-size: 1rem;
257
+ transition: all 0.3s ease;
258
+ backdrop-filter: blur(10px);
259
+ }
260
+
261
+ #change-speaker-btn:hover {
262
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.2), rgba(118, 75, 162, 0.2));
263
+ border-color: var(--animation-primary);
264
+ transform: translateY(-2px);
265
+ }
266
+
267
+ /* Modal Styles */
268
+ #speaker-modal {
269
+ position: fixed;
270
+ top: 0;
271
+ left: 0;
272
+ width: 100%;
273
+ height: 100%;
274
+ background: rgba(0,0,0,0.7);
275
+ backdrop-filter: blur(10px);
276
+ display: none;
277
+ align-items: center;
278
+ justify-content: center;
279
+ z-index: 1000;
280
+ opacity: 0;
281
+ transition: opacity 0.4s ease;
282
+ }
283
+
284
+ #speaker-modal.visible {
285
+ display: flex;
286
+ opacity: 1;
287
+ }
288
+
289
+ .modal-content {
290
+ background: rgba(255, 255, 255, 0.95);
291
+ backdrop-filter: blur(20px);
292
+ padding: 2.5rem;
293
+ border-radius: var(--radius-card);
294
+ width: 90%;
295
+ max-width: 800px;
296
+ max-height: 85vh;
297
+ overflow-y: auto;
298
+ transform: scale(0.9);
299
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
300
+ border: 1px solid rgba(255, 255, 255, 0.3);
301
+ }
302
+
303
+ #speaker-modal.visible .modal-content {
304
+ transform: scale(1);
305
+ }
306
+
307
+ .modal-header {
308
+ display: flex;
309
+ justify-content: space-between;
310
+ align-items: center;
311
+ margin-bottom: 2rem;
312
+ padding-bottom: 1rem;
313
+ border-bottom: 2px solid rgba(102, 126, 234, 0.2);
314
+ }
315
+
316
+ .modal-header h2 {
317
+ margin: 0;
318
+ font-size: 1.8em;
319
+ font-weight: 700;
320
+ color: var(--animation-primary);
321
+ }
322
+
323
+ .close-modal-btn {
324
+ background: none;
325
+ border: none;
326
+ font-size: 2.5rem;
327
+ cursor: pointer;
328
+ color: #999;
329
+ transition: all 0.3s ease;
330
+ width: 50px;
331
+ height: 50px;
332
+ border-radius: 50%;
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ }
337
+
338
+ .close-modal-btn:hover {
339
+ color: #333;
340
+ background: rgba(255, 0, 0, 0.1);
341
+ transform: rotate(90deg);
342
+ }
343
+
344
+ #speaker-grid {
345
+ display: grid;
346
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
347
+ gap: 1.5rem;
348
+ }
349
+
350
+ @media (min-width: 576px) {
351
+ #speaker-grid {
352
+ grid-template-columns: repeat(4, 1fr);
353
+ }
354
+ }
355
+
356
+ .speaker-card {
357
+ cursor: pointer;
358
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
359
+ }
360
+
361
+ .speaker-card .speaker-visual {
362
+ border: 3px solid transparent;
363
+ border-radius: var(--radius-card);
364
+ overflow: hidden;
365
+ text-align: center;
366
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
367
+ position: relative;
368
+ background: rgba(255, 255, 255, 0.9);
369
+ backdrop-filter: blur(10px);
370
+ }
371
+
372
+ .speaker-card:hover .speaker-visual {
373
+ transform: translateY(-8px) scale(1.02);
374
+ box-shadow: 0 15px 40px rgba(0,0,0,0.25);
375
+ }
376
+
377
+ .speaker-card input[type="radio"] {
378
+ display: none;
379
+ }
380
+
381
+ .speaker-card img {
382
+ width: 100%;
383
+ height: 140px;
384
+ object-fit: cover;
385
+ display: block;
386
+ transition: all 0.3s ease;
387
+ }
388
+
389
+ .speaker-card:hover img {
390
+ transform: scale(1.1);
391
+ }
392
+
393
+ .speaker-card .speaker-name {
394
+ padding: 1rem 0.8rem;
395
+ font-weight: 600;
396
+ font-size: 1rem;
397
+ background: rgba(255, 255, 255, 0.9);
398
+ }
399
+
400
+ .speaker-card input[type="radio"]:checked + .speaker-visual {
401
+ border-color: var(--animation-primary);
402
+ box-shadow: var(--speaker-selected-glow);
403
+ transform: scale(1.05);
404
+ }
405
+
406
+ /* Slider Styles */
407
+ .slider-container {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 1.5rem;
411
+ background: rgba(255, 255, 255, 0.5);
412
+ padding: 1rem;
413
+ border-radius: var(--radius-input);
414
+ backdrop-filter: blur(10px);
415
+ }
416
+
417
+ input[type="range"] {
418
+ flex-grow: 1;
419
+ cursor: pointer;
420
+ height: 8px;
421
+ border-radius: 4px;
422
+ background: linear-gradient(90deg, var(--animation-primary), var(--animation-secondary));
423
+ outline: none;
424
+ -webkit-appearance: none;
425
+ }
426
+
427
+ input[type="range"]::-webkit-slider-thumb {
428
+ -webkit-appearance: none;
429
+ width: 24px;
430
+ height: 24px;
431
+ border-radius: 50%;
432
+ background: white;
433
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
434
+ cursor: pointer;
435
+ border: 3px solid var(--animation-primary);
436
+ transition: all 0.3s ease;
437
+ }
438
+
439
+ input[type="range"]::-webkit-slider-thumb:hover {
440
+ transform: scale(1.2);
441
+ }
442
+
443
+ #temperature-value {
444
+ font-weight: 700;
445
+ background: rgba(255, 255, 255, 0.8);
446
+ padding: 0.5rem 1rem;
447
+ border-radius: 12px;
448
+ border: 2px solid rgba(102, 126, 234, 0.3);
449
+ min-width: 60px;
450
+ text-align: center;
451
+ font-size: 1.1rem;
452
+ color: var(--animation-primary);
453
+ }
454
+
455
+ /* Button Styles */
456
+ #generate-btn {
457
+ width: 100%;
458
+ padding: 1.5rem 2rem;
459
+ font-size: 1.3em;
460
+ font-weight: 700;
461
+ font-family: var(--app-font);
462
+ background: var(--app-button-bg);
463
+ color: white;
464
+ border: none;
465
+ border-radius: var(--radius-input);
466
+ cursor: pointer;
467
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
468
+ box-shadow: var(--shadow-button);
469
+ position: relative;
470
+ overflow: hidden;
471
+ }
472
+
473
+ #generate-btn::before {
474
+ content: '';
475
+ position: absolute;
476
+ top: 0;
477
+ left: -100%;
478
+ width: 100%;
479
+ height: 100%;
480
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
481
+ transition: left 0.5s;
482
+ }
483
+
484
+ #generate-btn:hover::before {
485
+ left: 100%;
486
+ }
487
+
488
+ #generate-btn:hover:not(:disabled) {
489
+ background: var(--app-button-hover-bg);
490
+ transform: translateY(-3px);
491
+ box-shadow: 0 12px 35px -5px rgba(102, 126, 234, 0.6);
492
  }
493
 
494
+ #generate-btn:disabled {
495
+ background: linear-gradient(135deg, #999, #777);
496
+ cursor: not-allowed;
497
+ box-shadow: none;
498
+ transform: none;
499
+ }
500
+
501
+ /* Output Section */
502
+ #output-section {
503
+ margin-top: 3rem;
504
+ padding: 2rem;
505
+ background: rgba(255, 255, 255, 0.9);
506
+ backdrop-filter: blur(20px);
507
+ border-radius: var(--radius-card);
508
+ min-height: 120px;
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ flex-direction: column;
513
+ gap: 1.5rem;
514
+ border: 2px solid rgba(102, 126, 234, 0.2);
515
+ position: relative;
516
+ overflow: hidden;
517
+ }
518
+
519
+ #status-message {
520
+ font-weight: 600;
521
+ color: var(--app-text-secondary);
522
+ font-size: 1.1rem;
523
+ text-align: center;
524
+ }
525
+
526
+ #audio-player {
527
+ width: 100%;
528
+ margin-top: 1rem;
529
+ display: none;
530
+ border-radius: 12px;
531
+ overflow: hidden;
532
+ }
533
+
534
+ /* Loading Animation */
535
+ .loading-overlay {
536
+ position: fixed;
537
+ top: 0;
538
+ left: 0;
539
+ width: 100%;
540
+ height: 100%;
541
+ background: rgba(102, 126, 234, 0.95);
542
+ backdrop-filter: blur(10px);
543
+ display: none;
544
+ align-items: center;
545
+ justify-content: center;
546
+ z-index: 2000;
547
+ opacity: 0;
548
+ transition: opacity 0.5s ease;
549
+ }
550
+
551
+ .loading-overlay.visible {
552
+ display: flex;
553
+ opacity: 1;
554
+ }
555
+
556
+ .loading-content {
557
+ text-align: center;
558
+ color: white;
559
+ }
560
+
561
+ .loading-animation {
562
+ width: 200px;
563
+ height: 200px;
564
+ margin: 0 auto 2rem;
565
+ position: relative;
566
+ }
567
+
568
+ .sound-wave {
569
+ position: absolute;
570
+ width: 100%;
571
+ height: 100%;
572
+ border-radius: 50%;
573
+ border: 3px solid rgba(255, 255, 255, 0.3);
574
+ animation: pulse 2s ease-in-out infinite;
575
+ }
576
+
577
+ .sound-wave:nth-child(1) { animation-delay: 0s; }
578
+ .sound-wave:nth-child(2) { animation-delay: 0.4s; }
579
+ .sound-wave:nth-child(3) { animation-delay: 0.8s; }
580
+
581
  @keyframes pulse {
582
+ 0% {
583
+ transform: scale(0.8);
584
+ opacity: 1;
585
+ }
586
+ 100% {
587
+ transform: scale(1.2);
588
+ opacity: 0;
589
+ }
590
+ }
591
+
592
+ .ai-icon {
593
+ position: absolute;
594
+ top: 50%;
595
+ left: 50%;
596
+ transform: translate(-50%, -50%);
597
+ font-size: 4rem;
598
+ animation: rotate 3s linear infinite;
599
+ }
600
+
601
+ .loading-text {
602
+ font-size: 1.8rem;
603
+ font-weight: 700;
604
+ margin-bottom: 1rem;
605
+ text-shadow: 0 2px 4px rgba(0,0,0,0.3);
606
+ }
607
+
608
+ .loading-subtext {
609
+ font-size: 1.2rem;
610
+ opacity: 0.9;
611
+ animation: fadeInOut 2s ease-in-out infinite;
612
+ }
613
+
614
+ @keyframes fadeInOut {
615
+ 0%, 100% { opacity: 0.6; }
616
  50% { opacity: 1; }
617
+ }
618
+
619
+ /* Responsive Design */
620
+ @media (max-width: 768px) {
621
+ .app-header {
622
+ padding: 3rem 1rem 4rem 1rem;
623
+ }
624
+
625
+ .app-header h1 {
626
+ font-size: 2.5em;
627
+ }
628
+
629
+ .main-content {
630
+ padding: 2rem;
631
+ margin: -3rem auto 1rem auto;
632
+ }
633
+
634
+ .form-group {
635
+ margin-bottom: 2rem;
636
+ }
637
+
638
+ label {
639
+ font-size: 1.1em;
640
+ }
641
+
642
+ #speaker-grid {
643
+ grid-template-columns: repeat(2, 1fr);
644
+ gap: 1rem;
645
+ }
646
+ }
647
+
648
+ @media (max-width: 480px) {
649
+ .app-header h1 {
650
+ font-size: 2em;
651
+ }
652
+
653
+ .app-header p {
654
+ font-size: 1.1em;
655
+ }
656
+
657
+ .main-content {
658
+ padding: 1.5rem;
659
+ }
660
+
661
+ #speaker-grid {
662
+ grid-template-columns: 1fr;
663
+ }
664
  }
665
  </style>
666
  </head>
667
  <body>
668
  <div class="container">
669
  <header class="app-header">
670
+ <h1>🎤 آلفا TTS</h1>
671
+ <p>✨ جادوی تبدیل متن به صدا، به سبک شما ✨</p>
672
  </header>
673
 
674
  <main class="main-content">
675
  <form id="tts-form">
676
  <div class="form-group">
677
+ <label for="text-input" data-icon="📝">متن برای تبدیل</label>
678
  <textarea id="text-input" rows="5" placeholder="اینجا متن خود را به فارسی وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
679
  </div>
680
+
681
  <div class="form-group">
682
+ <label for="prompt-input" data-icon="🗣️">سبک و لحن گفتار (اختیاری)</label>
683
  <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی">
684
  </div>
685
+
686
  <div class="form-group">
687
+ <label data-icon="🎤">گوینده منتخب</label>
688
  <div id="selected-speaker-display">
689
  <div id="selected-speaker-card">
690
  <img id="selected-speaker-img" src="" alt="عکس گوینده">
 
693
  <p>برای ��غییر، روی دکمه زیر کلیک کنید</p>
694
  </div>
695
  </div>
696
+ <button type="button" id="change-speaker-btn">🔄 تغییر گوینده</button>
697
  </div>
698
  </div>
699
+
700
  <div class="form-group">
701
+ <label for="temperature-slider" data-icon="🌡️">میزان خلاقیت صدا (0.1 تا 1.5)</label>
702
  <div class="slider-container">
703
  <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
704
  <span id="temperature-value">0.9</span>
705
  </div>
706
  </div>
707
+
708
  <button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button>
709
  </form>
710
 
711
  <div id="output-section">
712
+ <div id="status-message">✨ خروجی صدا در اینجا نمایش داده می‌شود</div>
 
 
 
 
 
 
 
 
 
713
  <audio id="audio-player" controls></audio>
714
  </div>
715
  </main>
716
  </div>
717
 
718
+ <!-- Loading Animation Overlay -->
719
+ <div class="loading-overlay" id="loading-overlay">
720
+ <div class="loading-content">
721
+ <div class="loading-animation">
722
+ <div class="sound-wave"></div>
723
+ <div class="sound-wave"></div>
724
+ <div class="sound-wave"></div>
725
+ <div class="ai-icon">🤖</div>
726
+ </div>
727
+ <div class="loading-text">در حال تبدیل متن به صدا</div>
728
+ <div class="loading-subtext">با هوش مصنوعی آلفا</div>
729
+ </div>
730
+ </div>
731
+
732
+ <!-- Speaker Selection Modal -->
733
  <div id="speaker-modal">
734
  <div class="modal-content">
735
  <div class="modal-header">
736
+ <h2>🎭 انتخاب گوینده</h2>
737
  <button type="button" class="close-modal-btn">×</button>
738
  </div>
739
  <div id="speaker-grid"></div>
740
  </div>
741
  </div>
742
+
743
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
744
 
745
  <script>
 
760
  const tempSlider = document.getElementById('temperature-slider');
761
  const tempValueSpan = document.getElementById('temperature-value');
762
  const generateBtn = document.getElementById('generate-btn');
 
 
763
  const statusMessage = document.getElementById('status-message');
764
  const audioPlayer = document.getElementById('audio-player');
765
+ const loadingOverlay = document.getElementById('loading-overlay');
766
 
767
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
768
  const speakerModal = document.getElementById('speaker-modal');
 
779
  function getImageUrl(speaker, index) {
780
  const gender = speaker.name.includes('(مرد)') ? 'men' : (speaker.name.includes('(زن)') ? 'women' : 'lego');
781
  const imageIndex = (index * 7 + 13) % 100;
782
+ return `https://randomuser.me/api/portraits/${gender}/${imageIndex}.jpg`;
783
  }
784
 
785
  function updateSelectedSpeakerDisplay(speakerId) {
786
  const speaker = getSpeakerById(speakerId);
787
  if (speaker) {
788
  const speakerIndex = speakers.findIndex(s => s.id === speakerId);
789
+ selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex);
790
  selectedSpeakerNameDisplay.textContent = speaker.name;
791
  selectedSpeakerIdStorage.value = speaker.id;
792
  }
 
798
  const card = document.createElement('label');
799
  card.className = 'speaker-card';
800
  card.setAttribute('for', `modal-speaker-${speaker.id}`);
801
+
802
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
803
 
804
  card.innerHTML = `
 
818
  });
819
  }
820
 
821
+ function showLoadingAnimation() {
822
+ loadingOverlay.classList.add('visible');
823
+ document.body.style.overflow = 'hidden';
824
+ }
825
+
826
+ function hideLoadingAnimation() {
827
+ loadingOverlay.classList.remove('visible');
828
+ document.body.style.overflow = 'auto';
829
+ }
830
+
831
+ // Event Listeners
832
  changeSpeakerBtn.addEventListener('click', () => {
833
  createSpeakerCardsInModal();
834
  speakerModal.classList.add('visible');
835
  });
836
+
837
  closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
838
+
839
  speakerModal.addEventListener('click', (e) => {
840
  if (e.target === speakerModal) {
841
  speakerModal.classList.remove('visible');
842
  }
843
  });
844
 
845
+ tempSlider.addEventListener('input', () => {
846
+ tempValueSpan.textContent = tempSlider.value;
847
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
 
849
  async function generateAudio(event) {
850
  event.preventDefault();
851
+
852
+ // Show loading animation
853
+ showLoadingAnimation();
854
+
855
+ generateBtn.disabled = true;
856
+ generateBtn.textContent = '⏳ در حال پردازش...';
857
+ statusMessage.textContent = '🔄 در حال ارسال درخواست به سرور...';
858
+ audioPlayer.style.display = 'none';
859
+ audioPlayer.src = '';
860
 
861
  const text = textInput.value;
 
 
 
 
 
862
  const prompt = promptInput.value;
863
  const temperature = parseFloat(tempSlider.value);
864
  const selectedSpeaker = selectedSpeakerIdStorage.value;
865
  const sessionHash = Math.random().toString(36).substring(2);
866
 
867
+ if (!text.trim()) {
868
+ statusMessage.textContent = '❌ خطا: متن ورودی نمی‌تواند خالی باشد.';
869
+ generateBtn.disabled = false;
870
+ generateBtn.textContent = '🚀 تولید و پخش صدا';
871
+ hideLoadingAnimation();
872
+ return;
873
+ }
874
+
875
  const payload = {
876
  fn_index: FN_INDEX,
877
  data: [false, null, text, prompt, selectedSpeaker, temperature],
 
891
  throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status}): ${errorBody}`);
892
  }
893
 
894
+ statusMessage.textContent = '⏰ در انتظار نتیجه از سرور...';
895
+
896
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
897
  const reader = dataResponse.body.getReader();
898
  const decoder = new TextDecoder();
 
902
  while (true) {
903
  const { value, done } = await reader.read();
904
  if (done) break;
905
+
906
  buffer += decoder.decode(value, { stream: true });
907
  const lines = buffer.split('\n');
908
  buffer = lines.pop();
909
+
910
  for (const line of lines) {
911
  if (!line.startsWith('data:')) continue;
912
+
913
  try {
914
  const data = JSON.parse(line.substring(5));
915
+ if (data.msg === 'process_generating') {
916
+ statusMessage.textContent = '🎵 سرور در حال تولید فایل صوتی است...';
917
+ }
918
  if (data.msg === 'process_completed') {
919
  if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
920
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
921
+ } else {
922
+ console.error("ساختار پیام موفقیت مورد انتظار نبود:", data);
923
+ }
924
  break;
925
  }
926
  } catch (e) { /* نادیده گرفتن خطاهای پارس */ }
 
929
  }
930
 
931
  if (finalFilePath) {
932
+ statusMessage.textContent = '✅ فایل صوتی با موفقیت تولید شد!';
933
  const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
934
  audioPlayer.src = audioUrl;
935
+ audioPlayer.style.display = 'block';
936
+
937
+ // Hide loading animation before playing audio
938
+ hideLoadingAnimation();
939
+
940
+ // Add a small delay then play
941
+ setTimeout(() => {
942
+ audioPlayer.play().catch(e => {
943
+ console.log('Auto-play prevented by browser policy');
944
+ });
945
+ }, 500);
946
  } else {
947
+ throw new Error('فایل صوتی از سرور دریافت نشد. کنسول را برای اطلاعات بیشتر بررسی کنید.');
948
  }
949
 
950
  } catch (error) {
951
  console.error('یک خطا در فرآیند رخ داد:', error);
952
+ statusMessage.textContent = `❌ یک خطا رخ داد: ${error.message}`;
953
+ hideLoadingAnimation();
954
+ } finally {
955
+ generateBtn.disabled = false;
956
+ generateBtn.textContent = '🚀 تولید و پخش صدا';
957
  }
958
  }
959
 
960
+ // Initialize
961
  updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
962
  form.addEventListener('submit', generateAudio);
963
+
964
+ // Add some interactive effects
965
+ document.querySelectorAll('input, textarea').forEach(element => {
966
+ element.addEventListener('focus', function() {
967
+ this.parentElement.style.transform = 'translateY(-2px)';
968
+ });
969
+
970
+ element.addEventListener('blur', function() {
971
+ this.parentElement.style.transform = 'translateY(0)';
972
+ });
973
+ });
974
+
975
+ // Add parallax effect to header
976
+ window.addEventListener('scroll', () => {
977
+ const scrolled = window.pageYOffset;
978
+ const header = document.querySelector('.app-header');
979
+ if (header) {
980
+ header.style.transform = `translateY(${scrolled * 0.5}px)`;
981
+ }
982
+ });
983
  });
984
  </script>
985
  </body>