Update index.html
Browse files- 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: #
|
13 |
-
--app-header-grad-end: #
|
14 |
--app-panel-bg: #FFFFFF;
|
15 |
-
--app-input-bg: #
|
16 |
-
--app-button-bg: #
|
17 |
-
--app-button-hover-bg: #
|
18 |
-
--app-main-bg:
|
19 |
-
--app-text-primary: #
|
20 |
-
--app-text-secondary: #
|
21 |
-
--app-border-color: #
|
22 |
--radius-card: 24px;
|
23 |
--radius-input: 12px;
|
24 |
-
--shadow-card: 0 10px 30px -5px rgba(
|
25 |
-
--shadow-button: 0 4px 15px -2px rgba(
|
26 |
-
--speaker-selected-glow: 0 0
|
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.
|
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
|
45 |
-
.app-header h1 { font-size:
|
46 |
-
.app-header p { font-size: 1.
|
47 |
-
.main-content { padding: 2.5rem; margin: -
|
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:
|
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.
|
51 |
-
textarea:focus, input[type="text"]:focus { outline: none; border-color: var(--app-button-bg); box-shadow: 0 0 0
|
52 |
|
53 |
/* --- نمایش گوینده منتخب --- */
|
54 |
#selected-speaker-display { text-align: center; }
|
55 |
-
#selected-speaker-card { display: inline-flex; align-items: center; background: #
|
56 |
-
#selected-speaker-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-left:
|
57 |
-
#selected-speaker-info h3 { margin: 0; font-size: 1.
|
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:
|
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(
|
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:
|
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: #
|
70 |
-
.close-modal-btn:hover { color:
|
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:
|
76 |
-
.speaker-card:hover .speaker-visual { transform: translateY(-
|
77 |
.speaker-card input[type="radio"] { display: none; }
|
78 |
-
.speaker-card img { width: 100%; height: 120px; object-fit: cover; display: block; background-color: #eee;
|
79 |
-
.speaker-card .speaker-name { padding: 0.
|
|
|
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:
|
84 |
-
input[type="range"] { flex-grow: 1;
|
85 |
-
|
86 |
-
|
87 |
-
#
|
88 |
-
#generate-btn
|
89 |
-
#
|
90 |
-
#
|
|
|
|
|
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 |
-
|
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;
|
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'),
|
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 |
-
|
247 |
-
|
248 |
-
generateBtn.disabled = true;
|
249 |
-
generateBtn.textContent = 'در حال پردازش...';
|
250 |
-
statusMessage.textContent = 'در حال ارسال درخواست به سرور...';
|
251 |
audioPlayer.style.display = 'none';
|
252 |
audioPlayer.src = '';
|
|
|
|
|
|
|
|
|
253 |
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
|
|
|
260 |
if (!text.trim()) {
|
261 |
-
|
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 |
-
|
328 |
-
audioPlayer.play();
|
329 |
} else {
|
330 |
-
throw new Error('فایل صوتی از سرور دریافت نشد.
|
331 |
}
|
332 |
|
333 |
} catch (error) {
|
334 |
console.error('یک خطا در فرآیند رخ داد:', error);
|
335 |
-
|
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>
|