File size: 4,002 Bytes
7017726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// ---- 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();
    }
  });
}