Hamed744 commited on
Commit
6b41f29
·
verified ·
1 Parent(s): 6acf0b3

Update index.html

Browse files
Files changed (1) hide show
  1. 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 { audioTotalTimeSpan.textContent = '0:00'; drawWaveform(0); } };
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); // 5 = barWidth(3) + barGap(2)
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
- 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 = []; } updatePlayerUI(); });
420
- let resizeTimeout; window.addEventListener('resize', () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (outputSection.classList.contains('has-content') && hiddenAudioPlayer.src) { hiddenAudioPlayer.dispatchEvent(new Event('loadedmetadata')); } }, 250); });
421
 
422
  // --- State Management Functions ---
423
- const showLoadingState = () => {
424
- audioPlayerContent.style.display = 'none'; outputSection.classList.remove('has-content');
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
- if (finalFilePath) {
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
- // Ensure default speaker exists in the updated list
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';