Update index.html
Browse files- index.html +22 -58
index.html
CHANGED
@@ -324,7 +324,6 @@
|
|
324 |
{ id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی", imgUrl: "https://uploadkon.ir/uploads/5d7805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۲۲۲.jpg" },
|
325 |
{ id: "Erinome", name: "روژان (زن)", desc: "شفاف و گویا", imgUrl: "https://uploadkon.ir/uploads/aa8805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۳۴۹.jpg" }, // Changed to female
|
326 |
{ id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت", imgUrl: "https://uploadkon.ir/uploads/a63c05_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۹۲۱.jpg" },
|
327 |
-
// { id: "Rasalthgeti", name: "الهه (زن)", desc: "اسرارآمیز و فریبنده" }, // REMOVED as per request
|
328 |
{ id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان", imgUrl: "https://uploadkon.ir/uploads/8bc405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۴۳۳.jpg" },
|
329 |
{ id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوشآهنگ", imgUrl: "https://uploadkon.ir/uploads/9cb405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۸۵۰.jpg" },
|
330 |
{ id: "Callirrhoe", name: "نیکو (زن)", desc: "روایتگر و قصهگو", imgUrl: "https://uploadkon.ir/uploads/ee5f05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۰۴۷.jpg" }, // Changed to female
|
@@ -348,7 +347,6 @@
|
|
348 |
|
349 |
// --- Speaker Logic ---
|
350 |
const getSpeakerById = (id) => speakers.find(s => s.id === id) || speakers[0];
|
351 |
-
// MODIFIED: getImageUrl now uses the pre-defined imgUrl from the speaker object
|
352 |
const getImageUrl = (speaker) => speaker.imgUrl;
|
353 |
|
354 |
const updateSelectedSpeakerDisplay = (speakerId) => { const speaker = getSpeakerById(speakerId); selectedSpeakerImgDisplay.src = getImageUrl(speaker); selectedSpeakerNameDisplay.textContent = speaker.name; selectedSpeakerDescDisplay.textContent = speaker.desc; selectedSpeakerIdStorage.value = speaker.id; };
|
@@ -357,10 +355,11 @@
|
|
357 |
// --- Modal Management ---
|
358 |
const showModal = (modalElement) => { modalElement.classList.add('visible'); setTimeout(() => modalElement.querySelector('button')?.focus(), 50); }; const hideModal = (modalElement) => modalElement.classList.remove('visible'); changeSpeakerBtn.addEventListener('click', () => { createSpeakerCardsInModal(); showModal(speakerModal); }); selectedSpeakerCard.addEventListener('click', () => { createSpeakerCardsInModal(); showModal(speakerModal); }); tempInfoIcon.addEventListener('click', () => showModal(infoModal)); document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', (e) => (e.target === o) && hideModal(o))); document.querySelectorAll('.close-modal-btn').forEach(b => b.addEventListener('click', () => hideModal(document.getElementById(b.dataset.modalId)))); document.addEventListener('keydown', (e) => (e.key === 'Escape') && document.querySelectorAll('.modal-overlay.visible').forEach(hideModal)); tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
|
359 |
|
360 |
-
// --- Original Audio Player Logic ---
|
361 |
const formatTime = (seconds) => { if (isNaN(seconds) || seconds < 0) return '0:00'; const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`; };
|
362 |
|
363 |
function drawWaveform(progressRatio = 0) {
|
|
|
364 |
const dpr = window.devicePixelRatio || 1;
|
365 |
audioWaveformCanvas.width = audioWaveformCanvas.offsetWidth * dpr;
|
366 |
audioWaveformCanvas.height = audioWaveformCanvas.offsetHeight * dpr;
|
@@ -369,11 +368,11 @@
|
|
369 |
const width = audioWaveformCanvas.offsetWidth;
|
370 |
const height = audioWaveformCanvas.offsetHeight;
|
371 |
|
|
|
|
|
372 |
waveformCtx.clearRect(0, 0, width, height);
|
373 |
|
374 |
-
const barWidth = 3;
|
375 |
-
const barGap = 2;
|
376 |
-
const totalBarAndGap = barWidth + barGap;
|
377 |
const numBars = Math.floor(width / totalBarAndGap);
|
378 |
const offset = (width - (numBars * totalBarAndGap)) / 2;
|
379 |
const activeBars = Math.floor(progressRatio * numBars);
|
@@ -391,12 +390,12 @@
|
|
391 |
}
|
392 |
}
|
393 |
|
394 |
-
const updatePlayerUI = () => { playIcon.style.display = hiddenAudioPlayer.paused || hiddenAudioPlayer.ended ? 'block' : 'none'; pauseIcon.style.display = !(hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) ? 'block' : 'none'; const currentTime = hiddenAudioPlayer.currentTime; const duration = hiddenAudioPlayer.duration; audioCurrentTimeSpan.textContent = formatTime(currentTime); if (isFinite(duration) && duration > 0) { audioTotalTimeSpan.textContent = formatTime(duration); drawWaveform(currentTime / duration); } else {
|
395 |
|
396 |
async function processAudioForWaveform(audioBuffer) {
|
397 |
if (!audioBuffer) { audioPeaks = []; return; }
|
398 |
const channelData = audioBuffer.getChannelData(0);
|
399 |
-
const numBars = Math.floor(audioWaveformCanvas.offsetWidth / 5);
|
400 |
const samplesPerBar = Math.floor(channelData.length / numBars);
|
401 |
const peaks = [];
|
402 |
for (let i = 0; i < numBars; i++) {
|
@@ -409,90 +408,55 @@
|
|
409 |
peaks.push(Math.min(1, Math.max(0, maxPeak * 1.5)));
|
410 |
}
|
411 |
audioPeaks = peaks;
|
412 |
-
|
|
|
413 |
}
|
414 |
|
415 |
playPauseBtn.addEventListener('click', () => hiddenAudioPlayer.paused ? hiddenAudioPlayer.play() : hiddenAudioPlayer.pause()); skipBackwardBtn.addEventListener('click', () => hiddenAudioPlayer.currentTime = Math.max(0, hiddenAudioPlayer.currentTime - 5)); skipForwardBtn.addEventListener('click', () => hiddenAudioPlayer.currentTime = Math.min(hiddenAudioPlayer.duration, hiddenAudioPlayer.currentTime + 5));
|
416 |
volumeBtn.addEventListener('click', () => { hiddenAudioPlayer.muted = !hiddenAudioPlayer.muted; volumeHighIcon.style.display = hiddenAudioPlayer.muted ? 'none' : 'block'; volumeMuteIcon.style.display = hiddenAudioPlayer.muted ? 'block' : 'none'; });
|
417 |
speedBtn.addEventListener('click', () => { currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length; const newSpeed = playbackSpeeds[currentPlaybackSpeedIndex]; hiddenAudioPlayer.playbackRate = newSpeed; speedBtn.textContent = `${newSpeed}x`; });
|
418 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI); hiddenAudioPlayer.addEventListener('play', updatePlayerUI); hiddenAudioPlayer.addEventListener('pause', updatePlayerUI); hiddenAudioPlayer.addEventListener('ended', () => { hiddenAudioPlayer.currentTime = 0; updatePlayerUI(); });
|
419 |
-
hiddenAudioPlayer.addEventListener('loadedmetadata', async () => { if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)(); try { const response = await fetch(hiddenAudioPlayer.src); const arrayBuffer = await response.arrayBuffer(); const decodedBuffer = await audioContext.decodeAudioData(arrayBuffer); await processAudioForWaveform(decodedBuffer); } catch (e) { console.error("Error decoding audio for waveform:", e); audioPeaks = []; } updatePlayerUI(); });
|
420 |
-
let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (outputSection.classList.contains('has-content') && hiddenAudioPlayer.src) { hiddenAudioPlayer.
|
421 |
|
422 |
// --- State Management Functions ---
|
423 |
-
const showLoadingState = () => {
|
424 |
-
|
425 |
-
statusMessage.style.display = 'none'; loadingAnimationWrapper.style.display = 'flex';
|
426 |
-
generateBtn.disabled = true;
|
427 |
-
generateBtn.innerHTML = ` <svg aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101" style="animation: spin 1s linear infinite;"> <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/> <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/> </svg> در حال پردازش... `;
|
428 |
-
};
|
429 |
-
const showResultState = (isSuccess, message = '') => { loadingAnimationWrapper.style.display = 'none'; if (isSuccess) { statusMessage.style.display = 'none'; audioPlayerContent.style.display = 'flex'; outputSection.classList.add('has-content'); } else { statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.'; statusMessage.style.display = 'block'; audioPlayerContent.style.display = 'none'; outputSection.classList.remove('has-content'); hiddenAudioPlayer.src = ''; audioPeaks = []; drawWaveform(0); } generateBtn.disabled = false; generateBtn.innerHTML = '✨ خلق صدا با آلفا'; };
|
430 |
|
431 |
// --- API Call ---
|
432 |
async function generateAudio(event) {
|
433 |
-
event.preventDefault();
|
434 |
-
showLoadingState();
|
435 |
-
|
436 |
const text = textInput.value;
|
437 |
if (!text.trim()) { showResultState(false, 'خطا: متن ورودی نمیتواند خالی باشد.'); return; }
|
438 |
if (text.length > MAX_CHARS) { showResultState(false, `خطا: طول متن بیش از ${MAX_CHARS.toLocaleString('fa-IR')} نویسه است.`); return; }
|
439 |
-
|
440 |
-
const payload = {
|
441 |
-
fn_index: FN_INDEX,
|
442 |
-
data: [false, null, text, promptInput.value, selectedSpeakerIdStorage.value, parseFloat(tempSlider.value)],
|
443 |
-
event_data: null,
|
444 |
-
session_hash: Math.random().toString(36).substring(2)
|
445 |
-
};
|
446 |
-
|
447 |
try {
|
448 |
const joinQueueResponse = await fetch(JOIN_QUEUE_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
449 |
if (!joinQueueResponse.ok) { throw new Error(`خطا در برقراری ارتباط با سرویس آلفا (کد: ${joinQueueResponse.status}).`); }
|
450 |
-
|
451 |
-
let finalFilePath = null;
|
452 |
-
const startTime = Date.now();
|
453 |
-
const timeoutDuration = 90000;
|
454 |
-
|
455 |
while (Date.now() - startTime < timeoutDuration) {
|
456 |
const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${payload.session_hash}`);
|
457 |
if (!dataResponse.ok) { throw new Error(`خطا در دریافت داده از سرویس (کد: ${dataResponse.status})`); }
|
458 |
-
const responseText = await dataResponse.text();
|
459 |
-
const lines = responseText.trim().split('\n');
|
460 |
-
|
461 |
for (const line of lines) {
|
462 |
if (!line.startsWith('data:')) continue;
|
463 |
try {
|
464 |
const data = JSON.parse(line.substring(5));
|
465 |
if (data.msg === 'process_completed') {
|
466 |
-
if (data.success && data.output?.data?.[0] && (data.output.data[0].name || data.output.data[0].path)) {
|
467 |
-
finalFilePath = data.output.data[0].name || data.output.data[0].path;
|
468 |
-
} else { throw new Error(data.output?.error || 'خطای ناشناخته در پردازش سرور.'); }
|
469 |
break;
|
470 |
-
} else if (data.msg === 'queue_full') {
|
471 |
-
throw new Error('صف پردازش پر است. لطفا کمی بعد تلاش کنید.');
|
472 |
-
}
|
473 |
} catch (e) { console.warn("Error parsing JSON from stream:", e, "Line:", line); }
|
474 |
}
|
475 |
if (finalFilePath) break;
|
476 |
await new Promise(resolve => setTimeout(resolve, 1000));
|
477 |
}
|
478 |
-
|
479 |
-
|
480 |
-
hiddenAudioPlayer.src = `${FILE_URL_BASE}${finalFilePath}`;
|
481 |
-
showResultState(true);
|
482 |
-
} else if (Date.now() - startTime >= timeoutDuration) {
|
483 |
-
throw new Error('پردازش بیش از حد طول کشید و متوقف شد.');
|
484 |
-
} else {
|
485 |
-
throw new Error('فایل صوتی از سرور دریافت نشد یا پردازش ناموفق بود.');
|
486 |
-
}
|
487 |
-
} catch (error) {
|
488 |
-
console.error('خطا در فرآیند تولید صدا:', error);
|
489 |
-
showResultState(false, error.message);
|
490 |
-
}
|
491 |
}
|
492 |
|
493 |
// --- Initial Setup ---
|
494 |
-
|
495 |
-
const defaultSpeakerId = speakers[0] ? speakers[0].id : "Charon"; // Fallback to Charon if list is empty (shouldn't happen)
|
496 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || defaultSpeakerId);
|
497 |
form.addEventListener('submit', generateAudio);
|
498 |
statusMessage.style.display = 'block'; loadingAnimationWrapper.style.display = 'none';
|
|
|
324 |
{ id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی", imgUrl: "https://uploadkon.ir/uploads/5d7805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۲۲۲.jpg" },
|
325 |
{ id: "Erinome", name: "روژان (زن)", desc: "شفاف و گویا", imgUrl: "https://uploadkon.ir/uploads/aa8805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۳۴۹.jpg" }, // Changed to female
|
326 |
{ id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت", imgUrl: "https://uploadkon.ir/uploads/a63c05_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۹۲۱.jpg" },
|
|
|
327 |
{ id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان", imgUrl: "https://uploadkon.ir/uploads/8bc405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۴۳۳.jpg" },
|
328 |
{ id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوشآهنگ", imgUrl: "https://uploadkon.ir/uploads/9cb405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۸۵۰.jpg" },
|
329 |
{ id: "Callirrhoe", name: "نیکو (زن)", desc: "روایتگر و قصهگو", imgUrl: "https://uploadkon.ir/uploads/ee5f05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۰۴۷.jpg" }, // Changed to female
|
|
|
347 |
|
348 |
// --- Speaker Logic ---
|
349 |
const getSpeakerById = (id) => speakers.find(s => s.id === id) || speakers[0];
|
|
|
350 |
const getImageUrl = (speaker) => speaker.imgUrl;
|
351 |
|
352 |
const updateSelectedSpeakerDisplay = (speakerId) => { const speaker = getSpeakerById(speakerId); selectedSpeakerImgDisplay.src = getImageUrl(speaker); selectedSpeakerNameDisplay.textContent = speaker.name; selectedSpeakerDescDisplay.textContent = speaker.desc; selectedSpeakerIdStorage.value = speaker.id; };
|
|
|
355 |
// --- Modal Management ---
|
356 |
const showModal = (modalElement) => { modalElement.classList.add('visible'); setTimeout(() => modalElement.querySelector('button')?.focus(), 50); }; const hideModal = (modalElement) => modalElement.classList.remove('visible'); changeSpeakerBtn.addEventListener('click', () => { createSpeakerCardsInModal(); showModal(speakerModal); }); selectedSpeakerCard.addEventListener('click', () => { createSpeakerCardsInModal(); showModal(speakerModal); }); tempInfoIcon.addEventListener('click', () => showModal(infoModal)); document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', (e) => (e.target === o) && hideModal(o))); document.querySelectorAll('.close-modal-btn').forEach(b => b.addEventListener('click', () => hideModal(document.getElementById(b.dataset.modalId)))); document.addEventListener('keydown', (e) => (e.key === 'Escape') && document.querySelectorAll('.modal-overlay.visible').forEach(hideModal)); tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
|
357 |
|
358 |
+
// --- Original Audio Player Logic (with Waveform Fix) ---
|
359 |
const formatTime = (seconds) => { if (isNaN(seconds) || seconds < 0) return '0:00'; const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`; };
|
360 |
|
361 |
function drawWaveform(progressRatio = 0) {
|
362 |
+
// This function is now robust because it's called inside requestAnimationFrame
|
363 |
const dpr = window.devicePixelRatio || 1;
|
364 |
audioWaveformCanvas.width = audioWaveformCanvas.offsetWidth * dpr;
|
365 |
audioWaveformCanvas.height = audioWaveformCanvas.offsetHeight * dpr;
|
|
|
368 |
const width = audioWaveformCanvas.offsetWidth;
|
369 |
const height = audioWaveformCanvas.offsetHeight;
|
370 |
|
371 |
+
if (width === 0 || height === 0) return; // Extra safety check
|
372 |
+
|
373 |
waveformCtx.clearRect(0, 0, width, height);
|
374 |
|
375 |
+
const barWidth = 3; const barGap = 2; const totalBarAndGap = barWidth + barGap;
|
|
|
|
|
376 |
const numBars = Math.floor(width / totalBarAndGap);
|
377 |
const offset = (width - (numBars * totalBarAndGap)) / 2;
|
378 |
const activeBars = Math.floor(progressRatio * numBars);
|
|
|
390 |
}
|
391 |
}
|
392 |
|
393 |
+
const updatePlayerUI = () => { playIcon.style.display = hiddenAudioPlayer.paused || hiddenAudioPlayer.ended ? 'block' : 'none'; pauseIcon.style.display = !(hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) ? 'block' : 'none'; const currentTime = hiddenAudioPlayer.currentTime; const duration = hiddenAudioPlayer.duration; audioCurrentTimeSpan.textContent = formatTime(currentTime); if (isFinite(duration) && duration > 0) { audioTotalTimeSpan.textContent = formatTime(duration); requestAnimationFrame(() => drawWaveform(currentTime / duration)); } else { requestAnimationFrame(() => drawWaveform(0)); } };
|
394 |
|
395 |
async function processAudioForWaveform(audioBuffer) {
|
396 |
if (!audioBuffer) { audioPeaks = []; return; }
|
397 |
const channelData = audioBuffer.getChannelData(0);
|
398 |
+
const numBars = Math.floor(audioWaveformCanvas.offsetWidth / 5);
|
399 |
const samplesPerBar = Math.floor(channelData.length / numBars);
|
400 |
const peaks = [];
|
401 |
for (let i = 0; i < numBars; i++) {
|
|
|
408 |
peaks.push(Math.min(1, Math.max(0, maxPeak * 1.5)));
|
409 |
}
|
410 |
audioPeaks = peaks;
|
411 |
+
// Draw initial waveform on the next available frame to ensure canvas is ready
|
412 |
+
requestAnimationFrame(() => drawWaveform(0));
|
413 |
}
|
414 |
|
415 |
playPauseBtn.addEventListener('click', () => hiddenAudioPlayer.paused ? hiddenAudioPlayer.play() : hiddenAudioPlayer.pause()); skipBackwardBtn.addEventListener('click', () => hiddenAudioPlayer.currentTime = Math.max(0, hiddenAudioPlayer.currentTime - 5)); skipForwardBtn.addEventListener('click', () => hiddenAudioPlayer.currentTime = Math.min(hiddenAudioPlayer.duration, hiddenAudioPlayer.currentTime + 5));
|
416 |
volumeBtn.addEventListener('click', () => { hiddenAudioPlayer.muted = !hiddenAudioPlayer.muted; volumeHighIcon.style.display = hiddenAudioPlayer.muted ? 'none' : 'block'; volumeMuteIcon.style.display = hiddenAudioPlayer.muted ? 'block' : 'none'; });
|
417 |
speedBtn.addEventListener('click', () => { currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length; const newSpeed = playbackSpeeds[currentPlaybackSpeedIndex]; hiddenAudioPlayer.playbackRate = newSpeed; speedBtn.textContent = `${newSpeed}x`; });
|
418 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI); hiddenAudioPlayer.addEventListener('play', updatePlayerUI); hiddenAudioPlayer.addEventListener('pause', updatePlayerUI); hiddenAudioPlayer.addEventListener('ended', () => { hiddenAudioPlayer.currentTime = 0; updatePlayerUI(); });
|
419 |
+
hiddenAudioPlayer.addEventListener('loadedmetadata', async () => { if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)(); try { const response = await fetch(hiddenAudioPlayer.src); const arrayBuffer = await response.arrayBuffer(); const decodedBuffer = await audioContext.decodeAudioData(arrayBuffer); await processAudioForWaveform(decodedBuffer); } catch (e) { console.error("Error decoding audio for waveform:", e); audioPeaks = []; requestAnimationFrame(() => drawWaveform(0)); } updatePlayerUI(); });
|
420 |
+
let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (outputSection.classList.contains('has-content') && hiddenAudioPlayer.src) { requestAnimationFrame(() => drawWaveform(hiddenAudioPlayer.currentTime / hiddenAudioPlayer.duration)); } }, 250); });
|
421 |
|
422 |
// --- State Management Functions ---
|
423 |
+
const showLoadingState = () => { audioPlayerContent.style.display = 'none'; outputSection.classList.remove('has-content'); statusMessage.style.display = 'none'; loadingAnimationWrapper.style.display = 'flex'; generateBtn.disabled = true; generateBtn.innerHTML = ` <svg aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101" style="animation: spin 1s linear infinite;"> <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/> <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/> </svg> در حال پردازش... `; };
|
424 |
+
const showResultState = (isSuccess, message = '') => { loadingAnimationWrapper.style.display = 'none'; if (isSuccess) { statusMessage.style.display = 'none'; audioPlayerContent.style.display = 'flex'; outputSection.classList.add('has-content'); } else { statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.'; statusMessage.style.display = 'block'; audioPlayerContent.style.display = 'none'; outputSection.classList.remove('has-content'); hiddenAudioPlayer.src = ''; audioPeaks = []; requestAnimationFrame(() => drawWaveform(0)); } generateBtn.disabled = false; generateBtn.innerHTML = '✨ خلق صدا با آلفا'; };
|
|
|
|
|
|
|
|
|
|
|
425 |
|
426 |
// --- API Call ---
|
427 |
async function generateAudio(event) {
|
428 |
+
event.preventDefault(); showLoadingState();
|
|
|
|
|
429 |
const text = textInput.value;
|
430 |
if (!text.trim()) { showResultState(false, 'خطا: متن ورودی نمیتواند خالی باشد.'); return; }
|
431 |
if (text.length > MAX_CHARS) { showResultState(false, `خطا: طول متن بیش از ${MAX_CHARS.toLocaleString('fa-IR')} نویسه است.`); return; }
|
432 |
+
const payload = { fn_index: FN_INDEX, data: [false, null, text, promptInput.value, selectedSpeakerIdStorage.value, parseFloat(tempSlider.value)], event_data: null, session_hash: Math.random().toString(36).substring(2) };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
433 |
try {
|
434 |
const joinQueueResponse = await fetch(JOIN_QUEUE_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
435 |
if (!joinQueueResponse.ok) { throw new Error(`خطا در برقراری ارتباط با سرویس آلفا (کد: ${joinQueueResponse.status}).`); }
|
436 |
+
let finalFilePath = null; const startTime = Date.now(); const timeoutDuration = 90000;
|
|
|
|
|
|
|
|
|
437 |
while (Date.now() - startTime < timeoutDuration) {
|
438 |
const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${payload.session_hash}`);
|
439 |
if (!dataResponse.ok) { throw new Error(`خطا در دریافت داده از سرویس (کد: ${dataResponse.status})`); }
|
440 |
+
const responseText = await dataResponse.text(); const lines = responseText.trim().split('\n');
|
|
|
|
|
441 |
for (const line of lines) {
|
442 |
if (!line.startsWith('data:')) continue;
|
443 |
try {
|
444 |
const data = JSON.parse(line.substring(5));
|
445 |
if (data.msg === 'process_completed') {
|
446 |
+
if (data.success && data.output?.data?.[0] && (data.output.data[0].name || data.output.data[0].path)) { finalFilePath = data.output.data[0].name || data.output.data[0].path; } else { throw new Error(data.output?.error || 'خطای ناشناخته در پردازش سرور.'); }
|
|
|
|
|
447 |
break;
|
448 |
+
} else if (data.msg === 'queue_full') { throw new Error('صف پردازش پر است. لطفا کمی بعد تلاش کنید.'); }
|
|
|
|
|
449 |
} catch (e) { console.warn("Error parsing JSON from stream:", e, "Line:", line); }
|
450 |
}
|
451 |
if (finalFilePath) break;
|
452 |
await new Promise(resolve => setTimeout(resolve, 1000));
|
453 |
}
|
454 |
+
if (finalFilePath) { hiddenAudioPlayer.src = `${FILE_URL_BASE}${finalFilePath}`; showResultState(true); } else if (Date.now() - startTime >= timeoutDuration) { throw new Error('پردازش بیش از حد طول کشید و متوقف شد.'); } else { throw new Error('فایل صوتی از سرور دریافت نشد یا پردازش ناموفق بود.'); }
|
455 |
+
} catch (error) { console.error('خطا در فرآیند تولید صدا:', error); showResultState(false, error.message); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
456 |
}
|
457 |
|
458 |
// --- Initial Setup ---
|
459 |
+
const defaultSpeakerId = speakers.length > 0 ? speakers[0].id : "Charon";
|
|
|
460 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || defaultSpeakerId);
|
461 |
form.addEventListener('submit', generateAudio);
|
462 |
statusMessage.style.display = 'block'; loadingAnimationWrapper.style.display = 'none';
|