// ---- Build signature waveform ---- const wave = document.getElementById('wave'); if (wave) { const BAR_COUNT = 24; for (let i = 0; i < BAR_COUNT; i++) { const bar = document.createElement('div'); bar.className = 'bar'; bar.style.animationDelay = (i * 0.06) + 's'; wave.appendChild(bar); } } const bars = wave ? wave.querySelectorAll('.bar') : []; function setWaveActive(active) { if (!wave) return; wave.classList.toggle('is-active', active); if (!active) bars.forEach(b => b.style.height = ''); } let waveInterval = null; function startWaveAnimation() { setWaveActive(true); waveInterval = setInterval(() => { bars.forEach(b => b.style.height = (6 + Math.random() * 28) + 'px'); }, 110); } function stopWaveAnimation() { if (waveInterval) clearInterval(waveInterval); waveInterval = null; setWaveActive(false); } // ---- Voice card selection ---- const voiceCards = document.querySelectorAll('.voice-card'); voiceCards.forEach(card => { card.addEventListener('click', () => { voiceCards.forEach(c => c.classList.remove('selected')); card.classList.add('selected'); }); }); // ---- Sliders ---- const rate = document.getElementById('rate'); const pitch = document.getElementById('pitch'); const rateVal = document.getElementById('rateVal'); const pitchVal = document.getElementById('pitchVal'); function fmt(v, unit) { const n = Number(v); return (n >= 0 ? '+' : '') + n + unit; } if (rate) rate.addEventListener('input', () => rateVal.textContent = fmt(rate.value, '%')); if (pitch) pitch.addEventListener('input', () => pitchVal.textContent = fmt(pitch.value, 'Hz')); // ---- Character counter ---- const MAX_CHARS = 10000; const text = document.getElementById('text'); const charCount = document.getElementById('charCount'); if (text) { text.addEventListener('input', () => { const len = text.value.length; charCount.textContent = `${len} / ${MAX_CHARS}`; charCount.classList.toggle('over', len > MAX_CHARS); }); } // ---- Generate ---- const generateBtn = document.getElementById('generateBtn'); const status = document.getElementById('status'); const output = document.getElementById('output'); const player = document.getElementById('player'); const downloadLink = document.getElementById('downloadLink'); let currentObjectUrl = null; if (generateBtn) { generateBtn.addEventListener('click', async () => { const value = text.value.trim(); if (!value) { status.textContent = 'Please enter some text first.'; status.classList.add('error'); return; } if (value.length > MAX_CHARS) { status.textContent = `Text is too long. Maximum is ${MAX_CHARS} characters.`; status.classList.add('error'); return; } const voice = document.querySelector('input[name="voice"]:checked').value; status.classList.remove('error'); status.textContent = 'Generating audio...'; generateBtn.disabled = true; startWaveAnimation(); try { const res = await fetch('/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: value, voice: voice, rate: Number(rate.value), pitch: Number(pitch.value) }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || 'Failed to generate audio.'); } const blob = await res.blob(); if (currentObjectUrl) URL.revokeObjectURL(currentObjectUrl); currentObjectUrl = URL.createObjectURL(blob); player.src = currentObjectUrl; downloadLink.href = currentObjectUrl; output.classList.add('visible'); status.textContent = 'Done! Your audio is ready below.'; player.play().catch(() => {}); } catch (e) { status.textContent = e.message; status.classList.add('error'); } finally { generateBtn.disabled = false; stopWaveAnimation(); } }); }