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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +122 -96
index.html CHANGED
@@ -9,21 +9,21 @@
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
- --app-header-grad-start: #1a2980;
13
- --app-header-grad-end: #26d0ce;
14
  --app-panel-bg: #FFFFFF;
15
- --app-input-bg: #F8F9FA;
16
- --app-button-bg: #5f27cd;
17
- --app-button-hover-bg: #481e9e;
18
- --app-main-bg: linear-gradient(170deg, #F3E8FF 0%, #E0F2FE 100%);
19
- --app-text-primary: #2c3e50;
20
- --app-text-secondary: #555;
21
- --app-border-color: #E0E0E0;
22
  --radius-card: 24px;
23
  --radius-input: 12px;
24
- --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.08);
25
- --shadow-button: 0 4px 15px -2px rgba(95, 39, 205, 0.4);
26
- --speaker-selected-glow: 0 0 15px rgba(95, 39, 205, 0.5);
27
  }
28
 
29
  body {
@@ -32,7 +32,7 @@
32
  background: var(--app-main-bg);
33
  color: var(--app-text-primary);
34
  font-size: 16px;
35
- line-height: 1.65;
36
  margin: 0;
37
  padding: 0;
38
  min-height: 100vh;
@@ -41,54 +41,71 @@
41
  }
42
 
43
  .container { max-width: 800px; width: 95%; margin: 0 auto; padding-bottom: 40px; }
44
- .app-header { padding: 3rem 1.5rem 5rem 1.5rem; text-align: center; background-image: linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%); color: white; border-bottom-left-radius: var(--radius-card); border-bottom-right-radius: var(--radius-card); box-shadow: 0 6px 20px -5px rgba(0,0,0,0.2); }
45
- .app-header h1 { font-size: 2.8em; font-weight: 800; margin:0 0 0.5rem 0; text-shadow: 0 2px 4px rgba(0,0,0,0.15); }
46
- .app-header p { font-size: 1.2em; color: rgba(255,255,255,0.9); margin-top:0; opacity: 0.9; }
47
- .main-content { padding: 2.5rem; margin: -3.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: 0.8rem; }
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.2s ease-in-out; }
51
- textarea:focus, input[type="text"]:focus { outline: none; border-color: var(--app-button-bg); box-shadow: 0 0 0 3px rgba(95, 39, 205, 0.2); background-color: #fff; }
52
 
53
  /* --- نمایش گوینده منتخب --- */
54
  #selected-speaker-display { text-align: center; }
55
- #selected-speaker-card { display: inline-flex; align-items: center; background: #fff; border-radius: 99px; padding: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); border: 2px solid var(--app-border-color); }
56
- #selected-speaker-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-left: 15px; background-color: #eee; }
57
- #selected-speaker-info h3 { margin: 0; font-size: 1.4em; }
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: 1rem auto 0; padding: 8px 20px; border-radius: 8px; 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; }
61
 
62
  /* --- مودال گالری گویندگان --- */
63
- #speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); 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: 80vh; 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; }
68
  .modal-header h2 { margin: 0; }
69
- .close-modal-btn { background: none; border: none; font-size: 2rem; cursor: pointer; color: #aaa; transition: color 0.2s ease; }
70
- .close-modal-btn:hover { color: #333; }
71
-
72
- #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; }
73
  @media (min-width: 576px) { #speaker-grid { grid-template-columns: repeat(4, 1fr); } }
74
- .speaker-card { cursor: pointer; transition: all 0.3s ease; }
75
- .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: var(--radius-card); overflow: hidden; text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.05); position: relative; background-color: #fff; }
76
- .speaker-card:hover .speaker-visual { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0,0,0,0.1); }
77
  .speaker-card input[type="radio"] { display: none; }
78
- .speaker-card img { width: 100%; height: 120px; object-fit: cover; display: block; background-color: #eee; /* Placeholder color */ }
79
- .speaker-card .speaker-name { padding: 0.7rem 0.5rem; font-weight: 500; font-size: 0.9em; }
 
80
  .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--app-button-bg); box-shadow: var(--speaker-selected-glow); }
 
81
 
82
  /* --- Slider & Button & Output --- */
83
- .slider-container { display: flex; align-items: center; gap: 1rem; }
84
- input[type="range"] { flex-grow: 1; cursor: pointer; }
85
- #temperature-value { font-weight: bold; background-color: var(--app-input-bg); padding: 0.2rem 0.8rem; border-radius: 8px; border: 1px solid var(--app-border-color); min-width: 40px; text-align: center; }
86
- #generate-btn { width: 100%; padding: 1rem 1.5rem; font-size: 1.2em; 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); }
87
- #generate-btn:hover:not(:disabled) { background-color: var(--app-button-hover-bg); transform: translateY(-2px); box-shadow: 0 6px 20px -3px rgba(95, 39, 205, 0.5); }
88
- #generate-btn:disabled { background-color: #999; cursor: not-allowed; box-shadow: none; }
89
- #output-section { margin-top: 2.5rem; padding: 1.5rem; background-color: #fff; border-radius: var(--radius-card); min-height: 100px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; border: 1px solid var(--app-border-color); }
90
- #status-message { font-weight: 500; color: var(--app-text-secondary); }
 
 
91
  #audio-player { width: 100%; margin-top: 1rem; display: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  </style>
93
  </head>
94
  <body>
@@ -108,7 +125,6 @@
108
  <label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری)</label>
109
  <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی">
110
  </div>
111
-
112
  <div class="form-group">
113
  <label>🎤 گوینده منتخب</label>
114
  <div id="selected-speaker-display">
@@ -122,7 +138,6 @@
122
  <button type="button" id="change-speaker-btn">تغییر گوینده</button>
123
  </div>
124
  </div>
125
-
126
  <div class="form-group">
127
  <label for="temperature-slider">🌡️ میزان خلاقیت صدا (0.1 تا 1.5)</label>
128
  <div class="slider-container">
@@ -130,12 +145,20 @@
130
  <span id="temperature-value">0.9</span>
131
  </div>
132
  </div>
133
-
134
  <button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button>
135
  </form>
136
 
137
  <div id="output-section">
138
  <div id="status-message">خروجی صدا در اینجا نمایش داده می‌شود</div>
 
 
 
 
 
 
 
 
 
139
  <audio id="audio-player" controls></audio>
140
  </div>
141
  </main>
@@ -171,46 +194,46 @@
171
  const tempSlider = document.getElementById('temperature-slider');
172
  const tempValueSpan = document.getElementById('temperature-value');
173
  const generateBtn = document.getElementById('generate-btn');
 
 
174
  const statusMessage = document.getElementById('status-message');
175
  const audioPlayer = document.getElementById('audio-player');
 
176
 
177
- const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage'); // برای نگهداری مقدار
178
  const speakerModal = document.getElementById('speaker-modal');
179
  const changeSpeakerBtn = document.getElementById('change-speaker-btn');
180
  const closeModalBtn = document.querySelector('.close-modal-btn');
181
- const speakerGridInModal = document.getElementById('speaker-grid'); // گرید داخل مودال
182
- const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img'); // نمایش عکس اصلی
183
- const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name'); // نمایش نام اصلی
184
 
185
  function getSpeakerById(id) {
186
  return speakers.find(s => s.id === id);
187
  }
188
 
189
  function getImageUrl(speaker, index) {
190
- // از یک سرویس عکس پرتره رندوم استفاده می کنیم
191
- // برای تنوع بیشتر، از یک اندیس متفاوت برای هر گوینده استفاده می کنیم
192
- const gender = speaker.name.includes('(مرد)') ? 'men' : (speaker.name.includes('(زن)') ? 'women' : 'lego'); // اگر جنسیت مشخص نیست، از لگو استفاده می کنیم
193
- const imageIndex = (index * 7 + 13) % 100; // یک فرمول ساده برای ایجاد تنوع در اندیس عکس
194
- return `https://randomuser.me/api/portraits/${gender}/${imageIndex}.jpg`;
195
  }
196
 
197
  function updateSelectedSpeakerDisplay(speakerId) {
198
  const speaker = getSpeakerById(speakerId);
199
  if (speaker) {
200
  const speakerIndex = speakers.findIndex(s => s.id === speakerId);
201
- selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex);
202
  selectedSpeakerNameDisplay.textContent = speaker.name;
203
- selectedSpeakerIdStorage.value = speaker.id; // ذخیره ID انتخاب شده
204
  }
205
  }
206
 
207
  function createSpeakerCardsInModal() {
208
- speakerGridInModal.innerHTML = ''; // پاک کردن گرید قبلی
209
  speakers.forEach((speaker, index) => {
210
  const card = document.createElement('label');
211
  card.className = 'speaker-card';
212
  card.setAttribute('for', `modal-speaker-${speaker.id}`);
213
-
214
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
215
 
216
  card.innerHTML = `
@@ -223,7 +246,7 @@
223
 
224
  card.addEventListener('click', () => {
225
  updateSelectedSpeakerDisplay(speaker.id);
226
- setTimeout(() => speakerModal.classList.remove('visible'), 100);
227
  });
228
 
229
  speakerGridInModal.appendChild(card);
@@ -231,7 +254,7 @@
231
  }
232
 
233
  changeSpeakerBtn.addEventListener('click', () => {
234
- createSpeakerCardsInModal(); // هر بار مودال باز می شود، کارت ها را با انتخاب فعلی می سازیم
235
  speakerModal.classList.add('visible');
236
  });
237
  closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
@@ -243,27 +266,45 @@
243
 
244
  tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
245
 
246
- async function generateAudio(event) {
247
- event.preventDefault();
248
- generateBtn.disabled = true;
249
- generateBtn.textContent = 'در حال پردازش...';
250
- statusMessage.textContent = 'در حال ارسال درخواست به سرور...';
251
  audioPlayer.style.display = 'none';
252
  audioPlayer.src = '';
 
 
 
 
253
 
254
- const text = textInput.value;
255
- const prompt = promptInput.value;
256
- const temperature = parseFloat(tempSlider.value);
257
- const selectedSpeaker = selectedSpeakerIdStorage.value; // از فیلد مخفی می‌خوانیم
258
- const sessionHash = Math.random().toString(36).substring(2);
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
 
260
  if (!text.trim()) {
261
- statusMessage.textContent = 'خطا: متن ورودی نمی‌تواند خالی باشد.';
262
- generateBtn.disabled = false;
263
- generateBtn.textContent = '🚀 تولید و پخش صدا';
264
  return;
265
  }
266
 
 
 
 
 
 
267
  const payload = {
268
  fn_index: FN_INDEX,
269
  data: [false, null, text, prompt, selectedSpeaker, temperature],
@@ -283,8 +324,6 @@
283
  throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status}): ${errorBody}`);
284
  }
285
 
286
- statusMessage.textContent = 'در انتظار نتیجه از سرور...';
287
-
288
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
289
  const reader = dataResponse.body.getReader();
290
  const decoder = new TextDecoder();
@@ -294,25 +333,17 @@
294
  while (true) {
295
  const { value, done } = await reader.read();
296
  if (done) break;
297
-
298
  buffer += decoder.decode(value, { stream: true });
299
  const lines = buffer.split('\n');
300
  buffer = lines.pop();
301
-
302
  for (const line of lines) {
303
  if (!line.startsWith('data:')) continue;
304
-
305
  try {
306
  const data = JSON.parse(line.substring(5));
307
- if (data.msg === 'process_generating') {
308
- statusMessage.textContent = 'سرور در حال تولید فایل صوتی است...';
309
- }
310
  if (data.msg === 'process_completed') {
311
  if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
312
  finalFilePath = data.output.data[0].name || data.output.data[0].path;
313
- } else {
314
- console.error("ساختار پیام موفقیت مورد انتظار نبود:", data);
315
- }
316
  break;
317
  }
318
  } catch (e) { /* نادیده گرفتن خطاهای پارس */ }
@@ -321,25 +352,20 @@
321
  }
322
 
323
  if (finalFilePath) {
324
- statusMessage.textContent = 'فایل صوتی با موفقیت دریافت شد!';
325
  const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
326
  audioPlayer.src = audioUrl;
327
- audioPlayer.style.display = 'block';
328
- audioPlayer.play();
329
  } else {
330
- throw new Error('فایل صوتی از سرور دریافت نشد. کنسول را برای اطلاعات بیشتر بررسی کنید.');
331
  }
332
 
333
  } catch (error) {
334
  console.error('یک خطا در فرآیند رخ داد:', error);
335
- statusMessage.textContent = `یک خطا رخ داد: ${error.message}`;
336
- } finally {
337
- generateBtn.disabled = false;
338
- generateBtn.textContent = '🚀 تولید و پخش صدا';
339
  }
340
  }
341
 
342
- updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value); // نمایش گوینده پیش‌فرض
343
  form.addEventListener('submit', generateAudio);
344
  });
345
  </script>
 
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
  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;
 
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>
 
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">
 
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">
 
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>
 
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');
205
  const changeSpeakerBtn = document.getElementById('change-speaker-btn');
206
  const closeModalBtn = document.querySelector('.close-modal-btn');
207
+ const speakerGridInModal = document.getElementById('speaker-grid');
208
+ const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
209
+ const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
210
 
211
  function getSpeakerById(id) {
212
  return speakers.find(s => s.id === id);
213
  }
214
 
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
  }
229
  }
230
 
231
  function createSpeakerCardsInModal() {
232
+ speakerGridInModal.innerHTML = '';
233
  speakers.forEach((speaker, index) => {
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 = `
 
246
 
247
  card.addEventListener('click', () => {
248
  updateSelectedSpeakerDisplay(speaker.id);
249
+ setTimeout(() => speakerModal.classList.remove('visible'), 200);
250
  });
251
 
252
  speakerGridInModal.appendChild(card);
 
254
  }
255
 
256
  changeSpeakerBtn.addEventListener('click', () => {
257
+ createSpeakerCardsInModal();
258
  speakerModal.classList.add('visible');
259
  });
260
  closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
 
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
  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
  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
  }
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>