USP / app.js
Phoe2004's picture
Upload 10 files
7017726 verified
Raw
History Blame Contribute Delete
4 kB
// ---- 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();
}
});
}