Viewing: tts.js
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('ttsForm');
const statusEl = document.getElementById('status');
const runBtn = document.getElementById('runBtn');
const previewBtn = document.getElementById('previewBtn');
const audioEl = document.getElementById('player');
const voiceSelect = document.getElementById('voice');
const languageSelect = document.getElementById('language');
const downloadEl = document.getElementById('downloadLink');
const metaEl = document.getElementById('meta');
const chips = Array.from(document.querySelectorAll('#voiceChips .chip'));
let lastUrl;
// --- Voice Data and Mappings ---
const allVoices = [
'Puck', 'Charon', 'Zephyr', 'Kore', 'Fenrir', 'Leda', 'Orus', 'Aoede',
'Callirhoe', 'Autonoe', 'Enceladus', 'Iapetus', 'Umbriel', 'Algieba',
'Despina', 'Erinome', 'Algenib', 'Rasalgethi', 'Laomedeia', 'Achernar',
'Alnilam', 'Schedar', 'Gacrux', 'Pulcherrima', 'Achird', 'Zubenelgenubi',
'Vindemiatrix', 'Sadachbia', 'Sadaltager', 'Sulafat'
];
// Recommended voices for specific languages
const voiceMappings = {
'zh-CN': ['Charon', 'Iapetus', 'Schedar', 'Sadaltager', 'Achernar', 'Sulafat'],
'zh-TW': ['Charon', 'Iapetus', 'Schedar', 'Sadaltager', 'Achernar', 'Sulafat'],
'ja-JP': ['Charon', 'Iapetus', 'Schedar', 'Sadaltager', 'Leda', 'Aoede'],
'ko-KR': ['Charon', 'Iapetus', 'Schedar', 'Sadaltager', 'Kore', 'Leda'],
'es-ES': ['Puck', 'Charon', 'Leda', 'Achird', 'Zubenelgenubi', 'Sulafat'],
'fr-FR': ['Puck', 'Charon', 'Leda', 'Aoede', 'Vindemiatrix', 'Sulafat'],
'de-DE': ['Charon', 'Kore', 'Orus', 'Iapetus', 'Alnilam', 'Sadaltager'],
};
function updateVoiceOptions() {
const selectedLanguage = languageSelect.value;
const currentVoice = voiceSelect.value;
const voices = voiceMappings[selectedLanguage] || allVoices;
voiceSelect.innerHTML = ''; // Clear existing options
voices.forEach(voice => {
const option = new Option(voice, voice);
voiceSelect.add(option);
});
// Try to preserve the previously selected voice if it's still in the list
if (voices.includes(currentVoice)) {
voiceSelect.value = currentVoice;
}
}
// Hide audio player initially if there's no src
if (!audioEl.getAttribute('src')) {
audioEl.style.display = 'none';
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
resetUi();
const formData = new FormData(form);
const text = (formData.get('text') || '').trim();
if (!text) {
statusEl.textContent = '่ฏท่พๅ
ฅ่ฆๆ่ฏป็ๆๆฌใ';
statusEl.classList.add('error');
return;
}
runBtn.disabled = true;
runBtn.textContent = '็ๆไธญ...';
try {
const payload = {
text,
model: (formData.get('model') || '').trim() || 'models/gemini-2.0-flash',
voice: formData.get('voice'),
language: formData.get('language'),
seed: formData.get('seed') ? Number(formData.get('seed')) : undefined
};
await requestAndPlay(payload, '็ๆๅฎๆ๏ผๅฏไปฅๆญๆพๆไธ่ฝฝใ');
} catch (error) {
statusEl.textContent = error.message || '่ฏทๆฑๅคฑ่ดฅ๏ผ่ฏท็จๅๅ่ฏใ';
statusEl.classList.add('error');
} finally {
runBtn.disabled = false;
runBtn.textContent = '็ๆ้ณ้ข';
}
});
// Preview current selection
if (previewBtn) {
previewBtn.addEventListener('click', async () => {
resetUi(false);
runBtn.disabled = true;
previewBtn.disabled = true;
previewBtn.textContent = '่ฏๅฌไธญ...';
try {
const modelInput = document.getElementById('model');
const textInput = document.getElementById('text');
const previewText = (textInput?.value || '').trim() || sampleText(languageSelect.value);
const payload = {
text: previewText,
model: (modelInput?.value || '').trim() || 'models/gemini-2.5-flash-preview-tts',
voice: voiceSelect.value,
language: languageSelect.value,
};
await requestAndPlay(payload, '่ฏๅฌๅฎๆ');
} catch (error) {
statusEl.textContent = error.message || '่ฏๅฌๅคฑ่ดฅ๏ผ่ฏท็จๅๅ่ฏใ';
statusEl.classList.add('error');
} finally {
runBtn.disabled = false;
previewBtn.disabled = false;
previewBtn.textContent = '่ฏๅฌๅฝๅ้ณ่ฒ';
}
});
}
// Quick chips for common voices
chips.forEach((chip) => {
chip.addEventListener('click', async () => {
voiceSelect.value = chip.dataset.voice;
if (previewBtn) previewBtn.click();
});
});
// --- Initial Setup ---
if (languageSelect) {
languageSelect.addEventListener('change', updateVoiceOptions);
updateVoiceOptions();
}
function inlineDataToWav(base64Data, mime) {
const binary = atob(base64Data);
const pcm = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) pcm[i] = binary.charCodeAt(i);
const rateMatch = /rate=(\d+)/.exec(mime);
const sampleRate = rateMatch ? Number(rateMatch[1]) : 24000;
return pcm16ToWave(pcm, sampleRate);
}
function pcm16ToWave(pcmBytes, sampleRate) {
const numChannels = 1;
const bitsPerSample = 16;
const headerSize = 44;
const dataLength = pcmBytes.byteLength;
const buffer = new ArrayBuffer(headerSize + dataLength);
const view = new DataView(buffer);
let offset = 0;
const writeString = (str) => { for (let i = 0; i < str.length; i++) view.setUint8(offset++, str.charCodeAt(i)); };
writeString('RIFF'); view.setUint32(offset, headerSize + dataLength - 8, true); offset += 4;
writeString('WAVE'); writeString('fmt '); view.setUint32(offset, 16, true); offset += 4;
view.setUint16(offset, 1, true); offset += 2; view.setUint16(offset, numChannels, true); offset += 2;
view.setUint32(offset, sampleRate, true); offset += 4;
const byteRate = sampleRate * numChannels * (bitsPerSample / 8); view.setUint32(offset, byteRate, true); offset += 4;
const blockAlign = numChannels * (bitsPerSample / 8); view.setUint16(offset, blockAlign, true); offset += 2;
view.setUint16(offset, bitsPerSample, true); offset += 2; writeString('data'); view.setUint32(offset, dataLength, true); offset += 4;
new Uint8Array(buffer, headerSize).set(pcmBytes);
return new Blob([buffer], { type: 'audio/wav' });
}
async function requestAndPlay(payload, successMessage) {
const resp = await fetch('tts-proxy.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await resp.json();
if (!resp.ok) {
const message = data?.error?.message || data?.error || `่ฏทๆฑๅคฑ่ดฅ (${resp.status})`;
throw new Error(message);
}
const candidate = data?.candidates?.[0];
// Check if the request was blocked for safety reasons.
if (candidate && candidate.finishReason === 'SAFETY') {
throw new Error('่ฏทๆฑๅ ๅฎๅ
จๅๅ ่ขซๆ็ปใ่ฏทๆฃๆฅๆๆฌๅ
ๅฎนๆฏๅฆๅ
ๅซไธๅฝไฟกๆฏใ');
}
const part = candidate?.content?.parts?.find((p) => p.inlineData);
if (!part?.inlineData?.data) {
throw new Error('ๆชๆถๅฐ้ณ้ขๆฐๆฎ๏ผ่ฏท้่ฏๆ็ผฉ็ญๆๆฌใ');
}
const inline = part.inlineData;
const mime = inline.mimeType || 'audio/L16;codec=pcm;rate=24000';
const blob = inlineDataToWav(inline.data, mime);
if (lastUrl) URL.revokeObjectURL(lastUrl);
lastUrl = URL.createObjectURL(blob);
audioEl.src = lastUrl;
audioEl.style.display = 'block';
audioEl.play().catch(() => {});
downloadEl.href = lastUrl;
downloadEl.style.display = 'inline-flex';
metaEl.textContent = mime;
statusEl.textContent = successMessage;
statusEl.classList.add('ok');
}
function resetUi(clearAudio = true) {
statusEl.textContent = '';
statusEl.className = 'tts-status';
downloadEl.style.display = 'none';
metaEl.textContent = '';
if (clearAudio) {
audioEl.src = '';
audioEl.style.display = 'none';
}
}
function sampleText(languageCode) {
switch (languageCode) {
case 'zh-CN': return 'ๆจๅฅฝ๏ผ่ฟๆฏ็คบไพ้ณ่ฒ่ฏๅฌใ';
case 'zh-TW': return 'ๆจๅฅฝ๏ผ้ๆฏ็คบไพ้ณ่ฒ่ฉฆ่ฝใ';
case 'ja-JP': return 'ใใใซใกใฏใใใใฏ้ณๅฃฐใฎใตใณใใซใงใใ';
case 'ko-KR': return '์๋
ํ์ธ์. ์ด๊ฒ์ ์์ฑ ์ํ์
๋๋ค.';
case 'es-ES': return 'Hola, esta es una muestra de voz.';
case 'fr-FR': return 'Bonjour, ceci est un รฉchantillon de voix.';
case 'de-DE': return 'Hallo, dies ist eine Sprachprobe.';
case 'hi-IN': return 'เคจเคฎเคธเฅเคคเฅ, เคฏเคน เคเคตเคพเคเคผ เคเคพ เคจเคฎเฅเคจเคพ เคนเฅเฅค';
case 'pt-BR': return 'Olรก, este รฉ um exemplo de voz.';
case 'en-GB': return 'Hello, this is a quick voice sample.';
case 'en-AU': return 'Gโday, this is a quick voice sample.';
default: return 'Hello, this is a quick voice sample.';
}
}
});
Close