Spaces:
Sleeping
Sleeping
| 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'); | |
| } | |
| }); | |