Hamed744 commited on
Commit
8d62790
·
verified ·
1 Parent(s): 76efbef

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +183 -188
index.html CHANGED
@@ -9,24 +9,25 @@
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
- --bg-color: #0d0f21;
13
- --panel-bg: rgba(26, 30, 56, 0.5);
14
- --panel-border: rgba(255, 255, 255, 0.15);
15
- --text-primary: #f0f4f9;
16
- --text-secondary: #a8b2d1;
17
- --accent-glow: #a77dfd;
18
- --accent-glow-rgb: 167, 125, 253;
19
- --button-bg: linear-gradient(135deg, rgba(167, 125, 253, 0.8) 0%, rgba(89, 97, 240, 0.8) 100%);
20
- --button-hover-bg: linear-gradient(135deg, rgba(167, 125, 253, 1) 0%, rgba(89, 97, 240, 1) 100%);
21
  --radius-card: 24px;
22
  --radius-input: 12px;
 
23
  }
24
 
25
- /* --- پس‌زمینه متحرک و زیبا --- */
26
  body {
27
  font-family: var(--app-font);
28
  direction: rtl;
29
- background-color: var(--bg-color);
 
30
  color: var(--text-primary);
31
  font-size: 16px;
32
  line-height: 1.7;
@@ -35,218 +36,202 @@
35
  min-height: 100vh;
36
  -webkit-font-smoothing: antialiased;
37
  -moz-osx-font-smoothing: grayscale;
38
- overflow: hidden;
39
- position: relative;
40
- }
41
-
42
- body::before {
43
- content: '';
44
- position: absolute;
45
- top: 0;
46
- left: 0;
47
- width: 100%;
48
- height: 100%;
49
- background:
50
- radial-gradient(circle at 15% 25%, rgba(var(--accent-glow-rgb), 0.2), transparent 40%),
51
- radial-gradient(circle at 85% 75%, rgba(89, 97, 240, 0.2), transparent 40%);
52
- animation: move-glow 20s infinite alternate;
53
- z-index: -1;
54
  }
55
 
56
- @keyframes move-glow {
57
- from { transform: translate(0, 0) scale(1); }
58
- to { transform: translate(50px, -50px) scale(1.2); }
 
 
 
 
 
 
59
  }
60
 
61
- .container { max-width: 800px; width: 95%; margin: 0 auto; z-index: 1; position: relative; }
62
-
63
- /* --- پنل اصلی با افکت شیشه‌ای --- */
64
- .main-panel {
65
- background: var(--panel-bg);
66
- border: 1px solid var(--panel-border);
67
- border-radius: var(--radius-card);
68
  backdrop-filter: blur(20px);
69
  -webkit-backdrop-filter: blur(20px);
70
- padding: 2rem 2.5rem;
71
- box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
 
 
72
  }
73
-
74
- .app-header { text-align: center; margin-bottom: 3rem; }
75
- .app-header h1 { font-size: 3em; font-weight: 800; margin:0; text-shadow: 0 0 20px rgba(var(--accent-glow-rgb), 0.5); }
76
- .app-header p { font-size: 1.25em; color: var(--text-secondary); margin-top:0.5rem; }
77
 
78
- .form-group { margin-bottom: 2.5rem; }
79
- label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; }
80
  textarea, input[type="text"] {
81
  width: 100%;
82
  padding: 1rem;
83
  border-radius: var(--radius-input);
84
- border: 1px solid var(--panel-border);
85
- background-color: rgba(0,0,0,0.2);
86
  box-shadow: none;
87
  font-family: var(--app-font);
88
  font-size: 1rem;
89
  box-sizing: border-box;
90
  color: var(--text-primary);
91
- transition: all 0.25s ease-in-out;
92
  }
93
  textarea:focus, input[type="text"]:focus {
94
  outline: none;
95
- border-color: var(--accent-glow);
96
- box-shadow: 0 0 15px rgba(var(--accent-glow-rgb), 0.3);
97
- background-color: rgba(0,0,0,0.3);
98
  }
99
 
100
- /* --- نمایش گوینده منتخب --- */
101
- #selected-speaker-display { text-align: center; }
102
- #selected-speaker-card { display: inline-flex; align-items: center; background: rgba(0,0,0,0.2); border-radius: 99px; padding: 10px; border: 1px solid var(--panel-border); }
103
- #selected-speaker-card img { width: 70px; height: 70px; border-radius: 50%; object-fit: cover; margin-left: 15px; border: 3px solid rgba(255,255,255,0.5); }
104
- #selected-speaker-info h3 { margin: 0; font-size: 1.4em; font-weight: 700; }
105
- #selected-speaker-info p { margin: 5px 0 0; color: var(--text-secondary); font-size: 0.9em; }
106
- #change-speaker-btn { display: block; margin: 1.2rem auto 0; padding: 10px 24px; border-radius: var(--radius-input); background-color: rgba(255,255,255,0.1); border: 1px solid var(--panel-border); cursor: pointer; font-family: var(--app-font); font-weight: 500; color: var(--text-primary); transition: all 0.2s ease; }
107
- #change-speaker-btn:hover { background-color: rgba(255,255,255,0.2); border-color: rgba(255,255,255,0.3); transform: translateY(-2px); }
 
 
 
 
 
 
 
108
 
109
- /* --- مودال گالری گویندگان --- */
110
- #speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(13, 15, 33, 0.7); backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; }
111
  #speaker-modal.visible { display: flex; opacity: 1; }
112
- .modal-content { background: var(--panel-bg); border: 1px solid var(--panel-border); 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; }
113
  #speaker-modal.visible .modal-content { transform: scale(1); }
114
- .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); }
115
- .modal-header h2 { margin: 0; }
116
- .close-modal-btn { background: none; border: none; font-size: 2.2rem; cursor: pointer; color: var(--text-secondary); transition: color 0.2s ease, transform 0.2s ease; line-height: 1; }
117
- .close-modal-btn:hover { color: var(--text-primary); transform: rotate(90deg); }
118
  #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.5rem; }
119
- .speaker-card { cursor: pointer; text-align: center; }
120
- .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: var(--radius-input); overflow: hidden; position: relative; background-color: rgba(0,0,0,0.2); transition: all 0.3s ease; }
121
- .speaker-card:hover .speaker-visual { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); }
122
- .speaker-card input[type="radio"] { display: none; }
123
- .speaker-card img { width: 100%; height: 120px; object-fit: cover; display: block; filter: saturate(0.8); transition: filter 0.3s; }
124
- .speaker-card:hover img { filter: saturate(1); }
125
- .speaker-card .speaker-name { padding: 0.8rem 0.5rem; font-weight: 500; font-size: 0.95em; color: var(--text-secondary); transition: color 0.2s; }
126
- .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--accent-glow); box-shadow: 0 0 20px rgba(var(--accent-glow-rgb), 0.4); }
127
- .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name { color: var(--accent-glow); font-weight: 700; }
128
-
129
- /* --- Slider & Button & Output --- */
130
- .slider-container { display: flex; align-items: center; gap: 1.5rem; }
131
- input[type="range"] { flex-grow: 1; -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: rgba(0,0,0,0.3); border-radius: 5px; outline: none; }
132
- input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: var(--accent-glow); border-radius: 50%; cursor: pointer; box-shadow: 0 0 10px rgba(var(--accent-glow-rgb), 0.7); }
133
- input[type="range"]::-moz-range-thumb { width: 22px; height: 22px; background: var(--accent-glow); border-radius: 50%; cursor: pointer; box-shadow: 0 0 10px rgba(var(--accent-glow-rgb), 0.7); }
134
- #temperature-value { font-weight: 700; background-color: rgba(0,0,0,0.3); padding: 0.4rem 1rem; border-radius: 8px; border: 1px solid var(--panel-border); min-width: 45px; text-align: center; }
135
- #generate-btn { width: 100%; padding: 1rem 1.5rem; font-size: 1.25em; font-weight: 700; font-family: var(--app-font); background: var(--button-bg); color: white; border: none; border-radius: var(--radius-input); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 5px 20px rgba(var(--accent-glow-rgb), 0.2); }
136
- #generate-btn:hover:not(:disabled) { background: var(--button-hover-bg); transform: translateY(-3px); box-shadow: 0 8px 25px rgba(var(--accent-glow-rgb), 0.4); }
137
- #generate-btn:disabled { background: rgba(107, 114, 128, 0.5); cursor: not-allowed; box-shadow: none; transform: none; }
138
- #output-section { margin-top: 2.5rem; padding: 2rem; min-height: 180px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; border-radius: var(--radius-input); background: rgba(0,0,0,0.2); border: 1px solid var(--panel-border); }
139
- #status-message { font-weight: 500; color: var(--text-secondary); text-align: center; }
140
- #audio-player { width: 100%; margin-top: 1rem; display: none; }
141
- audio::-webkit-media-controls-panel { background-color: rgba(40, 45, 80, 0.8); }
142
- audio::-webkit-media-controls-play-button { color: var(--accent-glow); }
143
- audio::-webkit-media-controls-current-time-display, audio::-webkit-media-controls-time-remaining-display { color: var(--text-primary); }
144
 
145
- /* --- انیمیشن پردازش فوق العاده زیبا --- */
146
- #premium-loader { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; }
147
- .loader-container { position: relative; width: 150px; height: 150px; }
148
- .loader-orb {
149
- position: absolute;
150
- top: 50%; left: 50%;
151
- width: 80px; height: 80px;
152
- background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%);
153
- border-radius: 50%;
154
- transform: translate(-50%, -50%);
155
- animation: orb-pulse 2.5s infinite ease-in-out;
156
  }
157
- .loader-particle {
158
- position: absolute;
159
- top: 50%; left: 50%;
160
- width: 5px; height: 5px;
161
- background: var(--accent-glow);
162
- border-radius: 50%;
163
- box-shadow: 0 0 10px var(--accent-glow), 0 0 20px var(--accent-glow);
164
- animation: orbit 4s infinite linear;
165
- }
166
- .loader-particle:nth-child(2) { animation-delay: -0.5s; width: 4px; height: 4px; }
167
- .loader-particle:nth-child(3) { animation-delay: -1s; width: 6px; height: 6px; }
168
- .loader-particle:nth-child(4) { animation-delay: -1.5s; }
169
- .loader-particle:nth-child(5) { animation-delay: -2s; width: 3px; height: 3px; }
170
- .loader-particle:nth-child(6) { animation-delay: -2.5s; }
171
- .loader-particle:nth-child(7) { animation-delay: -3s; width: 4px; height: 4px; }
172
- .loader-particle:nth-child(8) { animation-delay: -3.5s; }
173
- #loading-text { font-size: 1.1em; font-weight: 500; color: var(--text-primary); text-shadow: 0 0 10px rgba(var(--accent-glow-rgb), 0.3); }
174
 
175
- @keyframes orb-pulse {
176
- 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.8; }
177
- 50% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
- @keyframes orbit {
180
- from { transform: rotate(0deg) translateX(70px) rotate(0deg); }
181
- to { transform: rotate(360deg) translateX(70px) rotate(-360deg); }
182
  }
183
 
 
 
 
 
 
184
  </style>
185
  </head>
186
  <body>
187
  <div class="container">
188
- <div class="main-panel">
189
- <header class="app-header">
190
- <h1>آلفا TTS</h1>
191
- <p>صدای آینده، خلق شده توسط هوش مصنوعی</p>
192
- </header>
193
 
194
- <main>
 
195
  <form id="tts-form">
196
  <div class="form-group">
197
- <label for="text-input">📝 متن برای تبدیل</label>
198
- <textarea id="text-input" rows="5" placeholder="اینجا متن خود را به فارسی وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
199
  </div>
200
  <div class="form-group">
201
- <label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری)</label>
202
- <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی">
203
  </div>
204
  <div class="form-group">
205
- <label>🎤 گوینده منتخب</label>
206
- <div id="selected-speaker-display">
207
- <div id="selected-speaker-card">
208
- <img id="selected-speaker-img" src="" alt="عکس گوینده">
209
- <div id="selected-speaker-info">
210
- <h3 id="selected-speaker-name"></h3>
211
- <p>برای تغییر کلیک کنید</p>
212
- </div>
213
  </div>
214
- <button type="button" id="change-speaker-btn">تغییر گوینده</button>
215
  </div>
216
  </div>
217
  <div class="form-group">
218
- <label for="temperature-slider">🌡️ میزان خلاقیت صدا (0.1 تا 1.5)</label>
219
- <div class="slider-container">
220
  <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
221
  <span id="temperature-value">0.9</span>
222
  </div>
223
  </div>
224
- <button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button>
225
  </form>
226
-
227
- <div id="output-section">
228
- <div id="status-message">خروجی صدا در اینجا نمایش داده می‌شود</div>
229
- <!-- انیمیشن لودینگ پرمیوم -->
230
- <div id="premium-loader">
231
- <div class="loader-container">
232
- <div class="loader-orb"></div>
233
- <span class="loader-particle"></span><span class="loader-particle"></span><span class="loader-particle"></span>
234
- <span class="loader-particle"></span><span class="loader-particle"></span><span class="loader-particle"></span>
235
- <span class="loader-particle"></span><span class="loader-particle"></span>
236
- </div>
237
- <p id="loading-text">در حال تبدیل متن به صدا با هوش مصنوعی آلفا...</p>
238
- </div>
239
- <audio id="audio-player" controls></audio>
 
 
240
  </div>
241
- </main>
 
 
 
242
  </div>
243
  </div>
244
 
 
245
  <div id="speaker-modal">
246
  <div class="modal-content">
247
- <div class="modal-header">
248
  <h2>انتخاب گوینده</h2>
249
- <button type="button" class="close-modal-btn">×</button>
250
  </div>
251
  <div id="speaker-grid"></div>
252
  </div>
@@ -255,6 +240,8 @@
255
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
256
 
257
  <script>
 
 
258
  document.addEventListener('DOMContentLoaded', () => {
259
  const HF_SPACE_URL = "https://hamed744-ttspro.hf.space";
260
  const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`;
@@ -273,24 +260,28 @@
273
  const tempValueSpan = document.getElementById('temperature-value');
274
  const generateBtn = document.getElementById('generate-btn');
275
 
 
 
276
  const statusMessage = document.getElementById('status-message');
277
  const audioPlayer = document.getElementById('audio-player');
278
- const premiumLoader = document.getElementById('premium-loader');
279
 
280
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
281
  const speakerModal = document.getElementById('speaker-modal');
282
- const changeSpeakerBtn = document.getElementById('change-speaker-btn');
283
  const closeModalBtn = document.querySelector('.close-modal-btn');
284
  const speakerGridInModal = document.getElementById('speaker-grid');
285
  const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
286
  const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
287
 
288
- function getSpeakerById(id) { return speakers.find(s => s.id === id); }
 
 
289
 
290
  function getImageUrl(speaker, index) {
291
- const gender = speaker.name.includes('(مرد)') ? 'men' : (speaker.name.includes('(زن)') ? 'women' : 'lego');
292
- const imageIndex = (index * 11 + 7) % 100;
293
- return `https://randomuser.me/api/portraits/${gender}/${imageIndex}.jpg`;
294
  }
295
 
296
  function updateSelectedSpeakerDisplay(speakerId) {
@@ -310,20 +301,17 @@
310
  card.className = 'speaker-card';
311
  card.setAttribute('for', `modal-speaker-${speaker.id}`);
312
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
313
-
314
  card.innerHTML = `
315
- <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
316
  <div class="speaker-visual">
317
- <img src="${getImageUrl(speaker, index)}" alt="عکس گوینده ${speaker.name}" loading="lazy">
318
- <div class="speaker-name">${speaker.name}</div>
319
  </div>
320
  `;
321
-
322
  card.addEventListener('click', () => {
323
  updateSelectedSpeakerDisplay(speaker.id);
324
  setTimeout(() => speakerModal.classList.remove('visible'), 200);
325
  });
326
-
327
  speakerGridInModal.appendChild(card);
328
  });
329
  }
@@ -340,27 +328,27 @@
340
  tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
341
 
342
  function showLoadingState() {
 
343
  statusMessage.style.display = 'none';
344
  audioPlayer.style.display = 'none';
345
  audioPlayer.src = '';
346
- premiumLoader.style.display = 'flex';
347
  generateBtn.disabled = true;
348
- generateBtn.textContent = 'در حال خلق صدا...';
349
  }
350
 
351
  function showResultState(isSuccess, message = '') {
352
- premiumLoader.style.display = 'none';
353
  if (isSuccess) {
354
- statusMessage.style.display = 'none';
355
  audioPlayer.style.display = 'block';
356
  audioPlayer.play();
357
  } else {
358
  statusMessage.textContent = message || 'یک خطای ناشناخته رخ داد.';
359
  statusMessage.style.display = 'block';
360
- audioPlayer.style.display = 'none';
361
  }
362
  generateBtn.disabled = false;
363
- generateBtn.textContent = '🚀 تولید و پخش صدا';
364
  }
365
 
366
  async function generateAudio(event) {
@@ -377,11 +365,19 @@
377
  const temperature = parseFloat(tempSlider.value);
378
  const selectedSpeaker = selectedSpeakerIdStorage.value;
379
  const sessionHash = Math.random().toString(36).substring(2);
380
- const payload = { fn_index: FN_INDEX, data: [false, null, text, prompt, selectedSpeaker, temperature], event_data: null, session_hash: sessionHash };
 
 
 
 
 
 
381
 
382
  try {
383
- const joinQueueResponse = await fetch(JOIN_QUEUE_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
384
- if (!joinQueueResponse.ok) { throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status})`); }
 
 
385
 
386
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
387
  const reader = dataResponse.body.getReader();
@@ -405,14 +401,13 @@
405
  }
406
  break;
407
  }
408
- } catch (e) { /* ignore parse errors */ }
409
  }
410
  if (finalFilePath) break;
411
  }
412
 
413
  if (finalFilePath) {
414
- const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
415
- audioPlayer.src = audioUrl;
416
  showResultState(true);
417
  } else {
418
  throw new Error('فایل صوتی از سرور دریافت نشد.');
 
9
 
10
  :root {
11
  --app-font: 'Vazirmatn', sans-serif;
12
+ --bg-color-start: #111827;
13
+ --bg-color-end: #1f2937;
14
+ --glass-bg: rgba(31, 41, 55, 0.5);
15
+ --glass-border: rgba(255, 255, 255, 0.1);
16
+ --text-primary: #f9fafb;
17
+ --text-secondary: #9ca3af;
18
+ --accent-color: #2dd4bf;
19
+ --accent-color-hover: #5eead4;
20
+ --accent-glow: rgba(45, 212, 191, 0.3);
21
  --radius-card: 24px;
22
  --radius-input: 12px;
23
+ --shadow-card: 0 10px 30px -5px rgba(0,0,0,0.2);
24
  }
25
 
 
26
  body {
27
  font-family: var(--app-font);
28
  direction: rtl;
29
+ background-color: var(--bg-color-start);
30
+ background-image: radial-gradient(circle at top, var(--bg-color-end), var(--bg-color-start));
31
  color: var(--text-primary);
32
  font-size: 16px;
33
  line-height: 1.7;
 
36
  min-height: 100vh;
37
  -webkit-font-smoothing: antialiased;
38
  -moz-osx-font-smoothing: grayscale;
39
+ overflow-x: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
+ .container { max-width: 1200px; width: 95%; margin: 0 auto; }
43
+ .app-header { text-align: center; margin-bottom: 4rem; }
44
+ .app-header h1 { font-size: 3.5em; font-weight: 800; margin:0; color: var(--text-primary); text-shadow: 0 0 15px var(--accent-glow), 0 0 30px var(--accent-glow); }
45
+ .app-header p { font-size: 1.25em; color: var(--text-secondary); margin-top: 0.5rem; }
46
+
47
+ .main-grid {
48
+ display: grid;
49
+ grid-template-columns: 1.2fr 1fr;
50
+ gap: 2.5rem;
51
  }
52
 
53
+ /* --- پنل شیشه‌ای --- */
54
+ .main-content-glass {
55
+ background: var(--glass-bg);
 
 
 
 
56
  backdrop-filter: blur(20px);
57
  -webkit-backdrop-filter: blur(20px);
58
+ border: 1px solid var(--glass-border);
59
+ border-radius: var(--radius-card);
60
+ padding: 2.5rem;
61
+ box-shadow: var(--shadow-card);
62
  }
 
 
 
 
63
 
64
+ .form-group { margin-bottom: 2rem; }
65
+ label { display: block; font-weight: 600; color: var(--text-secondary); font-size: 1em; margin-bottom: 0.8rem; }
66
  textarea, input[type="text"] {
67
  width: 100%;
68
  padding: 1rem;
69
  border-radius: var(--radius-input);
70
+ border: 1px solid var(--glass-border);
71
+ background-color: rgba(17, 24, 39, 0.8);
72
  box-shadow: none;
73
  font-family: var(--app-font);
74
  font-size: 1rem;
75
  box-sizing: border-box;
76
  color: var(--text-primary);
77
+ transition: all 0.3s ease;
78
  }
79
  textarea:focus, input[type="text"]:focus {
80
  outline: none;
81
+ border-color: var(--accent-color);
82
+ background-color: rgba(31, 41, 55, 0.9);
83
+ box-shadow: 0 0 15px var(--accent-glow);
84
  }
85
 
86
+ /* --- گوینده منتخب --- */
87
+ #selected-speaker-card {
88
+ display: flex;
89
+ align-items: center;
90
+ background: rgba(17, 24, 39, 0.7);
91
+ border-radius: var(--radius-input);
92
+ padding: 1rem;
93
+ border: 1px solid var(--glass-border);
94
+ cursor: pointer;
95
+ transition: all 0.3s ease;
96
+ }
97
+ #selected-speaker-card:hover { border-color: var(--accent-color); background: rgba(31, 41, 55, 0.8); }
98
+ #selected-speaker-card img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-left: 15px; border: 2px solid var(--glass-border); }
99
+ #selected-speaker-info h3 { margin: 0; font-size: 1.3em; font-weight: 700; color: var(--text-primary); }
100
+ #selected-speaker-info p { margin: 2px 0 0; color: var(--text-secondary); font-size: 0.9em; }
101
 
102
+ /* --- مودال --- */
103
+ #speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(17, 24, 39, 0.7); backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; }
104
  #speaker-modal.visible { display: flex; opacity: 1; }
105
+ .modal-content { background: var(--glass-bg); border: 1px solid var(--glass-border); 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; box-shadow: 0 20px 50px rgba(0,0,0,0.3); }
106
  #speaker-modal.visible .modal-content { transform: scale(1); }
107
+ .modal-header h2 { color: var(--text-primary); }
108
+ .close-modal-btn { color: var(--text-secondary); }
 
 
109
  #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.5rem; }
110
+ .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: var(--radius-card); transition: all 0.3s ease; background: rgba(17, 24, 39, 0.8); }
111
+ .speaker-card:hover .speaker-visual { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); border-color: var(--accent-color); }
112
+ .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--accent-color); box-shadow: 0 0 20px var(--accent-glow); }
113
+ .speaker-card .speaker-name { color: var(--text-secondary); }
114
+ .speaker-card:hover .speaker-name, .speaker-card input[type="radio"]:checked + .speaker-visual .speaker-name { color: var(--text-primary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ /* --- Slider & Button --- */
117
+ input[type="range"] { flex-grow: 1; -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: rgba(17, 24, 39, 0.8); border-radius: 5px; outline: none; border: 1px solid var(--glass-border); }
118
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: var(--accent-color); border-radius: 50%; cursor: pointer; transition: all 0.2s; box-shadow: 0 0 10px var(--accent-glow); }
119
+ input[type="range"]::-webkit-slider-thumb:hover { background: var(--accent-color-hover); box-shadow: 0 0 20px var(--accent-glow); }
120
+ #temperature-value { font-weight: 700; color: var(--accent-color); min-width: 45px; text-align: center; }
121
+ #generate-btn {
122
+ width: 100%; padding: 1rem; font-size: 1.25em; font-weight: 700; font-family: var(--app-font);
123
+ background: var(--accent-color); color: var(--bg-color-start); border: none;
124
+ border-radius: var(--radius-input); cursor: pointer; transition: all 0.3s ease;
125
+ box-shadow: 0 0 20px var(--accent-glow);
 
126
  }
127
+ #generate-btn:hover:not(:disabled) { background: var(--accent-color-hover); transform: translateY(-3px) scale(1.02); box-shadow: 0 0 30px var(--accent-glow); }
128
+ #generate-btn:disabled { background: #4b5563; color: #9ca3af; cursor: not-allowed; box-shadow: none; transform: none; }
129
+
130
+ /* --- پنل خروجی و انیمیشن --- */
131
+ #output-panel { height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; position: relative; min-height: 400px; }
132
+ #output-initial-state { text-align: center; color: var(--text-secondary); }
133
+ #output-initial-state .icon { font-size: 4rem; margin-bottom: 1rem; display: block; opacity: 0.5; }
134
+ #audio-player { width: 100%; display: none; margin-top: 1rem; color-scheme: dark; }
 
 
 
 
 
 
 
 
 
135
 
136
+ /* --- انیمیشن هسته هوش مصنوعی --- */
137
+ #loading-animation { display: none; width: 100%; height: 100%; position: absolute; top: 0; left: 0; align-items: center; justify-content: center; flex-direction: column; gap: 2rem; }
138
+ .ai-core-container { position: relative; width: 200px; height: 200px; display: flex; align-items: center; justify-content: center; }
139
+ .ai-core {
140
+ width: 80px; height: 80px; background: radial-gradient(circle, var(--accent-color-hover), var(--accent-color) 70%);
141
+ border-radius: 50%; box-shadow: 0 0 30px var(--accent-color), 0 0 60px var(--accent-glow), inset 0 0 15px rgba(255,255,255,0.5);
142
+ animation: core-breath 4s infinite ease-in-out;
143
+ }
144
+ .energy-ring {
145
+ position: absolute; border-radius: 50%; border: 2px solid;
146
+ animation: rotate-ring 10s linear infinite;
147
+ }
148
+ .ring-1 { width: 150px; height: 150px; border-color: var(--accent-color) transparent transparent transparent; animation-duration: 8s; }
149
+ .ring-2 { width: 180px; height: 180px; border-color: transparent var(--accent-color) transparent transparent; animation-duration: 6s; animation-direction: reverse; }
150
+ .ring-3 { width: 210px; height: 210px; border-color: transparent transparent var(--accent-color) transparent; animation-duration: 12s; }
151
+ #loading-text { font-size: 1.1em; font-weight: 500; color: var(--text-secondary); text-shadow: 0 0 5px rgba(0,0,0,0.5); }
152
+ @keyframes core-breath {
153
+ 0%, 100% { transform: scale(1); box-shadow: 0 0 30px var(--accent-color), 0 0 60px var(--accent-glow); }
154
+ 50% { transform: scale(1.1); box-shadow: 0 0 45px var(--accent-color-hover), 0 0 90px var(--accent-glow); }
155
  }
156
+ @keyframes rotate-ring {
157
+ from { transform: rotate(0deg); }
158
+ to { transform: rotate(360deg); }
159
  }
160
 
161
+ /* --- Responsive --- */
162
+ @media (max-width: 992px) {
163
+ .main-grid { grid-template-columns: 1fr; }
164
+ #output-panel { margin-top: 2rem; }
165
+ }
166
  </style>
167
  </head>
168
  <body>
169
  <div class="container">
170
+ <header class="app-header">
171
+ <h1>Alpha TTS ᴾᴿᴱᴹᴵᵁ���</h1>
172
+ <p>کیفیت بی‌نظیر صدا، در یک تجربه‌ی استثنایی</p>
173
+ </header>
 
174
 
175
+ <div class="main-grid">
176
+ <div id="controls-panel" class="main-content-glass">
177
  <form id="tts-form">
178
  <div class="form-group">
179
+ <label for="text-input">متن شما</label>
180
+ <textarea id="text-input" rows="6" placeholder="متن مورد نظر برای تبدیل به صدا را اینجا وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea>
181
  </div>
182
  <div class="form-group">
183
+ <label for="prompt-input">فرمان لحن (اختیاری)</label>
184
+ <input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی حماسی و قدرتمند">
185
  </div>
186
  <div class="form-group">
187
+ <label>انتخاب گوینده</label>
188
+ <div id="selected-speaker-card" role="button" tabindex="0" aria-label="تغییر گوینده">
189
+ <img id="selected-speaker-img" src="" alt="عکس گوینده">
190
+ <div id="selected-speaker-info">
191
+ <h3 id="selected-speaker-name"></h3>
192
+ <p>برای تغییر کلیک کنید</p>
 
 
193
  </div>
 
194
  </div>
195
  </div>
196
  <div class="form-group">
197
+ <label for="temperature-slider">میزان خلاقیت</label>
198
+ <div class="slider-container" style="display: flex; align-items: center; gap: 1.5rem;">
199
  <input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
200
  <span id="temperature-value">0.9</span>
201
  </div>
202
  </div>
203
+ <button type="submit" id="generate-btn">ساخت صدا</button>
204
  </form>
205
+ </div>
206
+
207
+ <div id="output-panel" class="main-content-glass">
208
+ <div id="output-initial-state">
209
+ <span class="icon">♪</span>
210
+ <p>خروجی صدا در اینجا ظاهر می‌شود</p>
211
+ </div>
212
+
213
+ <div id="loading-animation">
214
+ <div class="ai-core-container">
215
+ <div class="energy-ring ring-1"></div>
216
+ <div class="energy-ring ring-2"></div>
217
+ <div class="energy-ring ring-3"></div>
218
+ <div class="ai-core"></div>
219
+ </div>
220
+ <p id="loading-text">در حال ساخت... هسته هوش مصنوعی فعال شد</p>
221
  </div>
222
+
223
+ <div id="status-message" style="display: none;"></div>
224
+ <audio id="audio-player" controls></audio>
225
+ </div>
226
  </div>
227
  </div>
228
 
229
+ <!-- Modal remains the same structurally, but will be styled by the new CSS -->
230
  <div id="speaker-modal">
231
  <div class="modal-content">
232
+ <div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--glass-border);">
233
  <h2>انتخاب گوینده</h2>
234
+ <button type="button" class="close-modal-btn" style="background: none; border: none; font-size: 2.2rem; cursor: pointer; transition: color 0.2s ease, transform 0.2s ease; line-height: 1;">×</button>
235
  </div>
236
  <div id="speaker-grid"></div>
237
  </div>
 
240
  <input type="hidden" id="selected_speaker_id_storage" value="Charon">
241
 
242
  <script>
243
+ // جاوا اسکریپت بدون تغییر باقی می‌ماند، چون منطق اصلی یکسان است
244
+ // اما برای هماهنگی با عناصر جدید، تغییراتی در نمایش وضعیت اعمال می‌کنیم
245
  document.addEventListener('DOMContentLoaded', () => {
246
  const HF_SPACE_URL = "https://hamed744-ttspro.hf.space";
247
  const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`;
 
260
  const tempValueSpan = document.getElementById('temperature-value');
261
  const generateBtn = document.getElementById('generate-btn');
262
 
263
+ // Output section elements
264
+ const outputInitialState = document.getElementById('output-initial-state');
265
  const statusMessage = document.getElementById('status-message');
266
  const audioPlayer = document.getElementById('audio-player');
267
+ const loadingAnimation = document.getElementById('loading-animation');
268
 
269
  const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
270
  const speakerModal = document.getElementById('speaker-modal');
271
+ const changeSpeakerBtn = document.getElementById('selected-speaker-card'); // Changed to the whole card
272
  const closeModalBtn = document.querySelector('.close-modal-btn');
273
  const speakerGridInModal = document.getElementById('speaker-grid');
274
  const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
275
  const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
276
 
277
+ function getSpeakerById(id) {
278
+ return speakers.find(s => s.id === id);
279
+ }
280
 
281
  function getImageUrl(speaker, index) {
282
+ const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
283
+ const imageIndex = (index * 7 + 13) % 100;
284
+ return `https://randomuser.me/api/portraits/women/${imageIndex}.jpg`;
285
  }
286
 
287
  function updateSelectedSpeakerDisplay(speakerId) {
 
301
  card.className = 'speaker-card';
302
  card.setAttribute('for', `modal-speaker-${speaker.id}`);
303
  const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
 
304
  card.innerHTML = `
305
+ <input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked} style="display:none;">
306
  <div class="speaker-visual">
307
+ <img src="${getImageUrl(speaker, index)}" alt="${speaker.name}" loading="lazy" style="width: 100%; height: 120px; object-fit: cover; display: block;">
308
+ <div class="speaker-name" style="padding: 0.8rem 0.5rem; font-weight: 500;">${speaker.name}</div>
309
  </div>
310
  `;
 
311
  card.addEventListener('click', () => {
312
  updateSelectedSpeakerDisplay(speaker.id);
313
  setTimeout(() => speakerModal.classList.remove('visible'), 200);
314
  });
 
315
  speakerGridInModal.appendChild(card);
316
  });
317
  }
 
328
  tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
329
 
330
  function showLoadingState() {
331
+ outputInitialState.style.display = 'none';
332
  statusMessage.style.display = 'none';
333
  audioPlayer.style.display = 'none';
334
  audioPlayer.src = '';
335
+ loadingAnimation.style.display = 'flex';
336
  generateBtn.disabled = true;
337
+ generateBtn.textContent = 'در حال پردازش...';
338
  }
339
 
340
  function showResultState(isSuccess, message = '') {
341
+ loadingAnimation.style.display = 'none';
342
  if (isSuccess) {
 
343
  audioPlayer.style.display = 'block';
344
  audioPlayer.play();
345
  } else {
346
  statusMessage.textContent = message || 'یک خطای ناشناخته رخ داد.';
347
  statusMessage.style.display = 'block';
348
+ statusMessage.style.color = '#f87171';
349
  }
350
  generateBtn.disabled = false;
351
+ generateBtn.textContent = 'ساخت صدا';
352
  }
353
 
354
  async function generateAudio(event) {
 
365
  const temperature = parseFloat(tempSlider.value);
366
  const selectedSpeaker = selectedSpeakerIdStorage.value;
367
  const sessionHash = Math.random().toString(36).substring(2);
368
+
369
+ const payload = {
370
+ fn_index: FN_INDEX,
371
+ data: [false, null, text, prompt, selectedSpeaker, temperature],
372
+ event_data: null,
373
+ session_hash: sessionHash
374
+ };
375
 
376
  try {
377
+ const joinQueueResponse = await fetch(JOIN_QUEUE_URL, {
378
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload)
379
+ });
380
+ if (!joinQueueResponse.ok) throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status})`);
381
 
382
  const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
383
  const reader = dataResponse.body.getReader();
 
401
  }
402
  break;
403
  }
404
+ } catch (e) {}
405
  }
406
  if (finalFilePath) break;
407
  }
408
 
409
  if (finalFilePath) {
410
+ audioPlayer.src = `${FILE_URL_BASE}${finalFilePath}`;
 
411
  showResultState(true);
412
  } else {
413
  throw new Error('فایل صوتی از سرور دریافت نشد.');