arabic-tts-api / static /script.js
AI Assistant
feat: Custom voice selector modal with audio preview and EQ animation
b619e9e
document.addEventListener('DOMContentLoaded', () => {
const HF_API = "https://bilalrhch-arabic-tts-api.hf.space";
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Voice definitions
// Add more voices here by uploading <id>.wav + <id>.txt to the HF /voices folder
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const availableVoices = [
{ id: "Auto", name: "ุชู„ู‚ุงุฆูŠ", nameEn: "Auto", gender: "ุฐูƒุงุก ุงุตุทู†ุงุนูŠ", color: "#4A90E2" },
{ id: "tariq", name: "ุทุงุฑู‚", nameEn: "Tariq", gender: "ุฑุฌู„", color: "#10B981" },
{ id: "omar", name: "ุนู…ุฑ", nameEn: "Omar", gender: "ุฑุฌู„", color: "#F59E0B" },
{ id: "layla", name: "ู„ูŠู„ู‰", nameEn: "Layla", gender: "ุงู…ุฑุฃุฉ", color: "#E11D48" },
{ id: "nour", name: "ู†ูˆุฑ", nameEn: "Nour", gender: "ุงู…ุฑุฃุฉ", color: "#8B5CF6" },
];
let selectedVoiceId = "Auto";
let previewAudio = null;
let playingVoiceId = null;
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Theme Toggle
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const themeToggle = document.getElementById('theme-toggle');
const moonIcon = document.getElementById('moon-icon');
const sunIcon = document.getElementById('sun-icon');
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
moonIcon.classList.add('hidden');
sunIcon.classList.remove('hidden');
}
themeToggle.addEventListener('click', () => {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
if (isDark) {
document.documentElement.removeAttribute('data-theme');
localStorage.setItem('theme', 'light');
moonIcon.classList.remove('hidden');
sunIcon.classList.add('hidden');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
moonIcon.classList.add('hidden');
sunIcon.classList.remove('hidden');
}
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Voice Selector Modal
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const voiceBtn = document.getElementById('custom-voice-btn');
const voiceModal = document.getElementById('voice-selector-modal');
const closeModalBtn = document.getElementById('close-modal-btn');
const voiceListEl = document.getElementById('voice-list-container');
function getInitial(voice) {
return voice.name.charAt(0);
}
function renderVoiceList() {
voiceListEl.innerHTML = '';
availableVoices.forEach(voice => {
const isActive = voice.id === selectedVoiceId;
const item = document.createElement('div');
item.className = 'voice-item' + (isActive ? ' active' : '');
item.dataset.voiceId = voice.id;
item.innerHTML = `
<div class="voice-item-left">
<div class="avatar" style="background-color: ${voice.color}">${getInitial(voice)}</div>
<div class="voice-info">
<span class="voice-name">${voice.name}</span>
<span class="voice-gender">${voice.gender}</span>
</div>
</div>
<div class="voice-item-right">
${voice.id !== 'Auto' ? `
<button type="button" class="preview-btn" data-voice-id="${voice.id}" title="ู…ุนุงูŠู†ุฉ ุงู„ุตูˆุช">
<div class="preview-audio-anim hidden" id="eq-${voice.id}">
<span></span><span></span><span></span>
</div>
<svg class="play-icon" id="play-icon-${voice.id}" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
</button>` : ''}
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
</div>
`;
// Select voice on row click (not the preview button)
item.addEventListener('click', (e) => {
if (e.target.closest('.preview-btn')) return;
selectVoice(voice);
closeModal();
});
voiceListEl.appendChild(item);
});
// Attach preview button handlers
voiceListEl.querySelectorAll('.preview-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const vId = btn.dataset.voiceId;
togglePreview(vId);
});
});
}
function selectVoice(voice) {
selectedVoiceId = voice.id;
// Update trigger button display
document.querySelector('#selected-voice-display .avatar').style.backgroundColor = voice.color;
document.querySelector('#selected-voice-display .avatar').textContent = getInitial(voice);
document.querySelector('#selected-voice-display .voice-name').textContent = voice.name;
document.querySelector('#selected-voice-display .voice-gender').textContent = voice.gender;
// Re-render to update active checkmark
renderVoiceList();
}
function openModal() {
renderVoiceList();
voiceModal.classList.remove('hidden');
voiceBtn.classList.add('open');
}
function closeModal() {
voiceModal.classList.add('hidden');
voiceBtn.classList.remove('open');
}
voiceBtn.addEventListener('click', () => {
if (voiceModal.classList.contains('hidden')) {
openModal();
} else {
closeModal();
}
});
closeModalBtn.addEventListener('click', closeModal);
// Close modal when clicking outside
document.addEventListener('click', (e) => {
if (!voiceModal.classList.contains('hidden') &&
!voiceBtn.contains(e.target) &&
!voiceModal.contains(e.target)) {
closeModal();
}
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Audio Preview
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function togglePreview(voiceId) {
if (playingVoiceId === voiceId) {
// Stop the current preview
stopPreview();
return;
}
// Stop previous if any
stopPreview();
const url = `${HF_API}/voices/${voiceId}.wav`;
previewAudio = new Audio(url);
playingVoiceId = voiceId;
// Show EQ animation, hide play icon
const eq = document.getElementById(`eq-${voiceId}`);
const playIcon = document.getElementById(`play-icon-${voiceId}`);
if (eq) eq.classList.remove('hidden');
if (playIcon) playIcon.classList.add('hidden');
previewAudio.play().catch(() => {
// Voice file not uploaded yet โ€” silently fail
stopPreview();
});
previewAudio.addEventListener('ended', stopPreview);
}
function stopPreview() {
if (previewAudio) {
previewAudio.pause();
previewAudio.currentTime = 0;
previewAudio = null;
}
if (playingVoiceId) {
const eq = document.getElementById(`eq-${playingVoiceId}`);
const playIcon = document.getElementById(`play-icon-${playingVoiceId}`);
if (eq) eq.classList.add('hidden');
if (playIcon) playIcon.classList.remove('hidden');
playingVoiceId = null;
}
}
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// TTS Logic
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const synthesizeBtn = document.getElementById('synthesize-btn');
const btnText = synthesizeBtn.querySelector('.btn-text');
const spinner = synthesizeBtn.querySelector('.spinner');
const textInput = document.getElementById('tts-text');
const speedInput = document.getElementById('tts-speed');
const resultContainer = document.getElementById('result-container');
const audioPlayer = document.getElementById('audio-player');
const downloadBtn = document.getElementById('download-btn');
const errorMessage = document.getElementById('error-message');
let currentAudioUrl = null;
synthesizeBtn.addEventListener('click', async () => {
const text = textInput.value.trim();
if (!text) {
showError("ุงู„ุฑุฌุงุก ุฅุฏุฎุงู„ ุงู„ู†ุต ุฃูˆู„ุงู‹");
return;
}
hideError();
resultContainer.classList.add('hidden');
setLoading(true);
stopPreview();
closeModal();
try {
const response = await fetch(`${HF_API}/synthesize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
voice: selectedVoiceId,
speed: parseFloat(speedInput.value) || 1.0
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => null);
throw new Error(errorData?.detail || `HTTP Error: ${response.status}`);
}
const data = await response.json();
const taskId = data.task_id;
document.getElementById('progress-container').classList.remove('hidden');
pollTaskStatus(taskId);
} catch (error) {
console.error("Synthesis error:", error);
showError(`ุญุฏุซ ุฎุทุฃ ุฃุซู†ุงุก ุงู„ุชูˆู„ูŠุฏ: ${error.message}`);
setLoading(false);
}
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Status Polling
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async function pollTaskStatus(taskId) {
try {
const statusRes = await fetch(`${HF_API}/status/${taskId}`);
if (!statusRes.ok) {
if (statusRes.status === 404 || statusRes.status === 503) {
throw new Error(`ุชุนุฐุฑ ุฌู„ุจ ุงู„ุญุงู„ุฉ (HTTP ${statusRes.status}). ุงู„ุฎุงุฏู… ู‚ูŠุฏ ุฅุนุงุฏุฉ ุงู„ุชุดุบูŠู„ุŒ ูŠุฑุฌู‰ ุงู„ู…ุญุงูˆู„ุฉ ุจุนุฏ ุฏู‚ูŠู‚ุฉ.`);
}
throw new Error(`Could not fetch status: HTTP ${statusRes.status}`);
}
const state = await statusRes.json();
document.getElementById('progress-fill').style.width = `${state.progress}%`;
let statusText = `ุฌุงุฑูŠ ุงู„ู…ุนุงู„ุฌุฉ... ${state.progress}%`;
if (state.status === "stitching") statusText = "ุฌุงุฑูŠ ุชุฌู…ูŠุน ุงู„ู…ู„ูุงุช ุงู„ุตูˆุชูŠุฉ...";
document.getElementById('progress-text').textContent = statusText;
if (state.status === "completed") {
document.getElementById('progress-container').classList.add('hidden');
if (currentAudioUrl && currentAudioUrl.startsWith('blob:')) {
URL.revokeObjectURL(currentAudioUrl);
}
currentAudioUrl = `${HF_API}/${state.download_url}`;
audioPlayer.src = currentAudioUrl;
resultContainer.classList.remove('hidden');
setLoading(false);
audioPlayer.play().catch(e => console.log('Autoplay blocked', e));
} else if (state.status === "failed") {
throw new Error(state.error || "Unknown background generation error");
} else {
setTimeout(() => pollTaskStatus(taskId), 2000);
}
} catch (error) {
console.error("Polling error:", error);
document.getElementById('progress-container').classList.add('hidden');
showError(`ุงู†ู‚ุทุน ุงู„ุงุชุตุงู„ ุฃูˆ ุญุฏุซ ุฎุทุฃ: ${error.message}`);
setLoading(false);
}
}
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Download
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
downloadBtn.addEventListener('click', () => {
if (!currentAudioUrl) return;
const a = document.createElement('a');
a.href = currentAudioUrl;
a.download = `OmniVoice_Arabic_${new Date().getTime()}.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Utils
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function setLoading(isLoading) {
synthesizeBtn.disabled = isLoading;
if (isLoading) {
btnText.classList.add('hidden');
spinner.classList.remove('hidden');
} else {
btnText.classList.remove('hidden');
spinner.classList.add('hidden');
}
}
function showError(msg) {
errorMessage.textContent = msg;
errorMessage.classList.remove('hidden');
}
function hideError() {
errorMessage.classList.add('hidden');
}
});