Spaces:
Runtime error
Runtime error
| // ---- 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(); | |
| } | |
| }); | |
| } | |