| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>HD TTS Document Reader | Hugging Face Edition</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); |
| } |
| |
| .waveform { |
| background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%); |
| height: 60px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 0 10px; |
| border-radius: 12px; |
| position: relative; |
| overflow: hidden; |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
| } |
| |
| .waveform-bar { |
| width: 3px; |
| background-color: rgba(255, 255, 255, 0.8); |
| border-radius: 3px; |
| animation: equalize 1.5s infinite ease-in-out; |
| animation-play-state: paused; |
| height: 10px; |
| animation-delay: calc(var(--delay) * 0.1s); |
| } |
| |
| @keyframes equalize { |
| 0%, 100% { height: 10px; } |
| 50% { height: 35px; } |
| } |
| |
| .document-container { |
| max-height: 400px; |
| overflow-y: auto; |
| scrollbar-width: thin; |
| } |
| |
| .document-container::-webkit-scrollbar { width: 8px; } |
| .document-container::-webkit-scrollbar-track { background: #f8fafc; border-radius: 10px; } |
| .document-container::-webkit-scrollbar-thumb { background: linear-gradient(145deg, #cbd5e1, #94a3b8); border-radius: 10px; } |
| |
| .highlight { |
| background-color: #fef08a; |
| border-radius: 4px; |
| padding: 2px 0; |
| transition: background-color 0.3s ease; |
| } |
| |
| .progress-bar { |
| height: 8px; |
| background-color: #e2e8f0; |
| border-radius: 10px; |
| overflow: hidden; |
| box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06); |
| cursor: pointer; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background: linear-gradient(90deg, #3b82f6, #8b5cf6); |
| width: 0%; |
| transition: width 0.1s linear; |
| border-radius: 10px; |
| } |
| |
| .glass-effect { |
| background: rgba(255, 255, 255, 0.9); |
| backdrop-filter: blur(20px); |
| border: 1px solid rgba(255, 255, 255, 0.2); |
| } |
| |
| .btn-primary { |
| background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); |
| transition: all 0.2s ease; |
| border-radius: 8px; |
| } |
| |
| .btn-primary:hover:not(:disabled) { |
| transform: translateY(-1px); |
| box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4); |
| } |
| .btn-primary:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| } |
| |
| .btn-secondary { |
| background: rgba(107, 114, 128, 0.1); |
| backdrop-filter: blur(10px); |
| transition: all 0.2s ease; |
| border-radius: 8px; |
| } |
| |
| .btn-secondary:hover:not(:disabled) { |
| background: rgba(107, 114, 128, 0.2); |
| transform: translateY(-1px); |
| } |
| .btn-secondary:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| } |
| |
| #document-editor:empty::before { |
| content: attr(data-placeholder); |
| color: #9ca3af; |
| font-style: italic; |
| } |
| |
| .card-hover { |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| .card-hover:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.1); |
| } |
| |
| input[type="range"] { |
| -webkit-appearance: none; appearance: none; |
| background: transparent; cursor: pointer; |
| } |
| input[type="range"]::-webkit-slider-track { background: #e2e8f0; height: 8px; border-radius: 5px; } |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; appearance: none; |
| height: 20px; width: 20px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); |
| cursor: pointer; border: 2px solid white; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| } |
| input[type="range"]::-moz-range-track { background: #e2e8f0; height: 8px; border-radius: 5px; } |
| input[type="range"]::-moz-range-thumb { |
| height: 20px; width: 20px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); |
| cursor: pointer; border: 2px solid white; |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| } |
| |
| .notification { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| background: rgba(255, 255, 255, 0.95); |
| backdrop-filter: blur(10px); |
| border-left: 4px solid #3b82f6; |
| padding: 16px 20px; |
| border-radius: 12px; |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); |
| transform: translateX(120%); |
| opacity: 0; |
| transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); |
| z-index: 1000; |
| min-width: 300px; |
| } |
| |
| .notification.show { transform: translateX(0); opacity: 1; } |
| .notification.success { border-left-color: #10b981; } |
| .notification.error { border-left-color: #ef4444; } |
| .notification.warning { border-left-color: #f59e0b; } |
| </style> |
| </head> |
| <body class="min-h-screen"> |
| <div class="container mx-auto px-4 py-8 max-w-7xl"> |
| <header class="mb-8 text-center"> |
| <h1 class="text-5xl font-bold text-gray-800 mb-4">HD TTS Document Reader</h1> |
| <p class="text-xl text-gray-600">Powered by the Hugging Face Inference API</p> |
| <div class="mt-6 flex justify-center gap-4 flex-wrap"> |
| <span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800"> |
| <i class="fas fa-microchip mr-2"></i> Hugging Face API |
| </span> |
| <span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> |
| <i class="fas fa-microphone-alt mr-2"></i> High-Quality Voice |
| </span> |
| <span class="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-green-100 text-green-800"> |
| <i class="fas fa-server mr-2"></i> Static Hosting Ready |
| </span> |
| </div> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="lg:col-span-2 glass-effect rounded-2xl shadow-xl overflow-hidden card-hover"> |
| <div class="p-8"> |
| <h2 class="text-3xl font-semibold text-gray-800 mb-6">Document Editor</h2> |
| |
| <div class="mb-6"> |
| <div class="document-container bg-gray-50 p-6 rounded-xl border border-gray-200"> |
| <div id="document-editor" class="min-h-[250px] outline-none prose max-w-none" contenteditable="true" data-placeholder="Type or paste your text here..."></div> |
| </div> |
| </div> |
| |
| <div class="flex justify-between items-center"> |
| <div class="text-sm text-gray-500"> |
| <span id="word-count">0</span> words • <span id="char-count">0</span> characters |
| </div> |
| <button id="clear-btn" class="text-gray-500 hover:text-red-600 font-medium transition-colors"><i class="fas fa-trash-alt mr-1"></i> Clear</button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="glass-effect rounded-2xl shadow-xl overflow-hidden card-hover"> |
| <div class="p-8"> |
| <h2 class="text-3xl font-semibold text-gray-800 mb-6">Voice Settings</h2> |
| |
| <div class="mb-6"> |
| <label for="hf-token" class="block text-sm font-medium text-gray-700 mb-2">Hugging Face API Token</label> |
| <div class="flex gap-2"> |
| <input type="password" id="hf-token" placeholder="Enter your HF Read Token" class="w-full border border-gray-300 rounded-lg px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> |
| <button id="save-token-btn" class="btn-primary text-white px-4 py-2 rounded-lg" title="Save Token"><i class="fas fa-save"></i></button> |
| </div> |
| <a href="https://huggingface.co/settings/tokens" target="_blank" class="text-xs text-blue-600 hover:underline mt-1">Get your token here</a> |
| </div> |
| |
| <div class="mb-8 p-4 bg-gray-100 rounded-xl border border-gray-200"> |
| <p class="text-sm font-medium text-gray-700">Active Voice Model:</p> |
| <p id="model-name" class="font-semibold text-gray-900">espnet/kan-bayashi_ljspeech_vits</p> |
| </div> |
| |
| <div class="mb-8"> |
| <label for="voice-speed" class="block text-sm font-medium text-gray-700 mb-3">Playback Speed: <span id="speed-value" class="font-semibold">1.0</span>x</label> |
| <input type="range" id="voice-speed" min="0.5" max="2" step="0.1" value="1" class="w-full cursor-pointer"> |
| </div> |
|
|
| <div class="flex flex-col gap-4 mb-6"> |
| <button id="play-btn" class="btn-primary text-white font-semibold py-4 px-6 rounded-lg transition flex items-center justify-center text-lg shadow-lg"> |
| <i id="play-icon" class="fas fa-play mr-3"></i> <span id="play-text">Play</span> |
| </button> |
| <div class="flex gap-4"> |
| <button id="pause-btn" class="flex-1 btn-secondary text-gray-800 font-semibold py-3 px-4 rounded-lg transition flex items-center justify-center" disabled> |
| <i class="fas fa-pause mr-2"></i> Pause |
| </button> |
| <button id="stop-btn" class="flex-1 btn-secondary text-gray-800 font-semibold py-3 px-4 rounded-lg transition flex items-center justify-center" disabled> |
| <i class="fas fa-stop mr-2"></i> Stop |
| </button> |
| </div> |
| </div> |
|
|
| <div class="progress-bar mb-1" id="progress-bar-container"> |
| <div id="progress-fill" class="progress-fill"></div> |
| </div> |
| <div class="flex justify-between text-xs text-gray-500"> |
| <span id="current-time">0:00</span> |
| <span id="total-time">0:00</span> |
| </div> |
|
|
| <div class="waveform mt-6" id="waveform"> |
| |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="notification-container" class="fixed top-5 right-5 z-[1001] w-80"></div> |
| |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const dom = { |
| editor: document.getElementById('document-editor'), |
| wordCount: document.getElementById('word-count'), |
| charCount: document.getElementById('char-count'), |
| playBtn: document.getElementById('play-btn'), |
| playIcon: document.getElementById('play-icon'), |
| playText: document.getElementById('play-text'), |
| pauseBtn: document.getElementById('pause-btn'), |
| stopBtn: document.getElementById('stop-btn'), |
| clearBtn: document.getElementById('clear-btn'), |
| speedSlider: document.getElementById('voice-speed'), |
| speedValue: document.getElementById('speed-value'), |
| progressBarContainer: document.getElementById('progress-bar-container'), |
| progressFill: document.getElementById('progress-fill'), |
| currentTime: document.getElementById('current-time'), |
| totalTime: document.getElementById('total-time'), |
| waveform: document.getElementById('waveform'), |
| notificationContainer: document.getElementById('notification-container'), |
| hfTokenInput: document.getElementById('hf-token'), |
| saveTokenBtn: document.getElementById('save-token-btn'), |
| modelName: document.getElementById('model-name'), |
| }; |
| |
| |
| const state = { |
| isPlaying: false, |
| isPaused: false, |
| isLoading: false, |
| currentSentenceIndex: 0, |
| sentences: [], |
| audioPlayer: new Audio(), |
| hfToken: '', |
| apiModel: 'espnet/kan-bayashi_ljspeech_vits', |
| }; |
| |
| |
| function init() { |
| loadToken(); |
| updateCounts(); |
| createWaveformBars(); |
| setupEventListeners(); |
| dom.modelName.textContent = state.apiModel; |
| } |
| |
| function loadToken() { |
| const savedToken = localStorage.getItem('hf_tts_token'); |
| if (savedToken) { |
| state.hfToken = savedToken; |
| dom.hfTokenInput.value = '********'; |
| showNotification('Hugging Face token loaded from storage.', 'success'); |
| } |
| } |
| |
| function createWaveformBars() { |
| dom.waveform.innerHTML = ''; |
| for (let i = 0; i < 40; i++) { |
| const bar = document.createElement('div'); |
| bar.className = 'waveform-bar'; |
| bar.style.setProperty('--delay', i); |
| dom.waveform.appendChild(bar); |
| } |
| } |
| |
| |
| function setupEventListeners() { |
| dom.editor.addEventListener('input', updateCounts); |
| dom.playBtn.addEventListener('click', handlePlayPause); |
| dom.pauseBtn.addEventListener('click', handlePause); |
| dom.stopBtn.addEventListener('click', handleStop); |
| dom.clearBtn.addEventListener('click', clearDocument); |
| dom.saveTokenBtn.addEventListener('click', saveToken); |
| dom.speedSlider.addEventListener('input', handleSpeedChange); |
| state.audioPlayer.addEventListener('timeupdate', updateProgress); |
| state.audioPlayer.addEventListener('ended', onAudioEnded); |
| } |
| |
| |
| |
| function handlePlayPause() { |
| if (state.isPlaying) { |
| handlePause(); |
| } else { |
| handlePlay(); |
| } |
| } |
| |
| function handlePlay() { |
| if (state.isLoading) return; |
| |
| if (state.isPaused) { |
| state.audioPlayer.play(); |
| state.isPaused = false; |
| state.isPlaying = true; |
| updateUIForPlaying(); |
| return; |
| } |
| |
| if (!state.hfToken) { |
| showNotification('Please enter your Hugging Face API Token.', 'error'); |
| dom.hfTokenInput.focus(); |
| return; |
| } |
| |
| const text = dom.editor.innerText.trim(); |
| if (!text) { |
| showNotification('Editor is empty. Please enter some text.', 'warning'); |
| return; |
| } |
| |
| state.sentences = text.match(/[^.!?]+[.!?]*|[^.!?]+/g) || []; |
| state.currentSentenceIndex = 0; |
| |
| if (state.sentences.length > 0) { |
| playSentence(state.currentSentenceIndex); |
| } |
| } |
| |
| function handlePause() { |
| if (state.isPlaying && !state.isPaused) { |
| state.audioPlayer.pause(); |
| state.isPaused = true; |
| state.isPlaying = false; |
| updateUIForPaused(); |
| } |
| } |
| |
| function handleStop() { |
| state.audioPlayer.pause(); |
| state.audioPlayer.src = ''; |
| resetState(); |
| updateUIForStopped(); |
| } |
| |
| async function playSentence(index) { |
| if (index >= state.sentences.length) { |
| handleStop(); |
| return; |
| } |
| |
| const sentence = state.sentences[index].trim(); |
| if (!sentence) { |
| |
| state.currentSentenceIndex++; |
| playSentence(state.currentSentenceIndex); |
| return; |
| } |
| |
| setLoading(true); |
| highlightSentence(index); |
| |
| try { |
| const response = await fetch(`https://api-inference.huggingface.co/models/${state.apiModel}`, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${state.hfToken}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ inputs: sentence }), |
| }); |
| |
| if (!response.ok) { |
| let errorMessage = `API Error: ${response.status} ${response.statusText}`; |
| try { |
| const errorDetails = await response.json(); |
| errorMessage = errorDetails.error || JSON.stringify(errorDetails); |
| } catch (e) { |
| |
| errorMessage = await response.text(); |
| } |
| throw new Error(errorMessage); |
| } |
| |
| const audioBlob = await response.blob(); |
| const audioUrl = URL.createObjectURL(audioBlob); |
| |
| state.audioPlayer.src = audioUrl; |
| state.audioPlayer.playbackRate = parseFloat(dom.speedSlider.value); |
| state.audioPlayer.play(); |
| |
| state.isPlaying = true; |
| state.isPaused = false; |
| updateUIForPlaying(); |
| |
| } catch (error) { |
| console.error('TTS Error:', error); |
| showNotification(error.message, 'error'); |
| handleStop(); |
| } finally { |
| setLoading(false); |
| } |
| } |
| |
| function onAudioEnded() { |
| state.currentSentenceIndex++; |
| playSentence(state.currentSentenceIndex); |
| } |
| |
| |
| |
| function updateUIForPlaying() { |
| dom.playBtn.disabled = true; |
| dom.pauseBtn.disabled = false; |
| dom.stopBtn.disabled = false; |
| dom.playText.textContent = 'Playing...'; |
| dom.playIcon.className = 'fas fa-spinner fa-spin mr-3'; |
| animateWaveform(true); |
| } |
| |
| function updateUIForPaused() { |
| dom.playBtn.disabled = false; |
| dom.pauseBtn.disabled = true; |
| dom.stopBtn.disabled = false; |
| dom.playText.textContent = 'Resume'; |
| dom.playIcon.className = 'fas fa-play mr-3'; |
| animateWaveform(false); |
| } |
| |
| function updateUIForStopped() { |
| dom.playBtn.disabled = false; |
| dom.pauseBtn.disabled = true; |
| dom.stopBtn.disabled = true; |
| dom.playText.textContent = 'Play'; |
| dom.playIcon.className = 'fas fa-play mr-3'; |
| animateWaveform(false); |
| clearHighlight(); |
| dom.progressFill.style.width = '0%'; |
| dom.currentTime.textContent = '0:00'; |
| dom.totalTime.textContent = '0:00'; |
| } |
| |
| function setLoading(isLoading) { |
| state.isLoading = isLoading; |
| dom.playBtn.disabled = isLoading; |
| if (isLoading) { |
| dom.playIcon.className = 'fas fa-spinner fa-spin mr-3'; |
| dom.playText.textContent = 'Loading...'; |
| } else { |
| dom.playIcon.className = 'fas fa-play mr-3'; |
| dom.playText.textContent = 'Play'; |
| } |
| } |
| |
| function resetState() { |
| state.isPlaying = false; |
| state.isPaused = false; |
| state.isLoading = false; |
| state.currentSentenceIndex = 0; |
| state.sentences = []; |
| } |
| |
| function highlightSentence(index) { |
| clearHighlight(); |
| let content = dom.editor.innerHTML; |
| const sentenceToHighlight = state.sentences[index]; |
| |
| |
| |
| const highlightedContent = content.replace(sentenceToHighlight, `<span class="highlight">${sentenceToHighlight}</span>`); |
| dom.editor.innerHTML = highlightedContent; |
| } |
| |
| function clearHighlight() { |
| const highlights = dom.editor.querySelectorAll('.highlight'); |
| highlights.forEach(h => { |
| h.outerHTML = h.innerHTML; |
| }); |
| } |
| |
| function saveToken() { |
| const token = dom.hfTokenInput.value; |
| if (token && token !== '********') { |
| state.hfToken = token; |
| localStorage.setItem('hf_tts_token', token); |
| dom.hfTokenInput.value = '********'; |
| showNotification('API Token saved successfully!', 'success'); |
| } else if (!token) { |
| showNotification('Please enter a token to save.', 'warning'); |
| } |
| } |
| |
| function updateProgress() { |
| if (state.audioPlayer.duration) { |
| const progress = (state.audioPlayer.currentTime / state.audioPlayer.duration) * 100; |
| dom.progressFill.style.width = `${progress}%`; |
| dom.currentTime.textContent = formatTime(state.audioPlayer.currentTime); |
| dom.totalTime.textContent = formatTime(state.audioPlayer.duration); |
| } |
| } |
| |
| function handleSpeedChange(e) { |
| const newSpeed = parseFloat(e.target.value); |
| dom.speedValue.textContent = newSpeed.toFixed(1); |
| if (state.isPlaying || state.isPaused) { |
| state.audioPlayer.playbackRate = newSpeed; |
| } |
| } |
| |
| function clearDocument() { |
| handleStop(); |
| dom.editor.innerHTML = ''; |
| updateCounts(); |
| showNotification('Document cleared.', 'success'); |
| } |
| |
| function updateCounts() { |
| const text = dom.editor.innerText; |
| const words = text.trim().split(/\s+/).filter(Boolean).length; |
| dom.charCount.textContent = text.length; |
| dom.wordCount.textContent = words; |
| } |
| |
| function animateWaveform(isPlaying) { |
| const state = isPlaying ? 'running' : 'paused'; |
| dom.waveform.querySelectorAll('.waveform-bar').forEach(bar => { |
| bar.style.animationPlayState = state; |
| }); |
| } |
| |
| function showNotification(message, type = 'info') { |
| const notification = document.createElement('div'); |
| notification.className = `notification ${type}`; |
| const icons = { info: 'fa-info-circle', success: 'fa-check-circle', warning: 'fa-exclamation-triangle', error: 'fa-times-circle' }; |
| notification.innerHTML = `<div class="flex items-center"><i class="fas ${icons[type]} text-lg mr-3"></i><span>${message}</span></div>`; |
| dom.notificationContainer.appendChild(notification); |
| setTimeout(() => notification.classList.add('show'), 100); |
| setTimeout(() => { |
| notification.classList.remove('show'); |
| setTimeout(() => notification.remove(), 500); |
| }, 5000); |
| } |
| |
| function formatTime(seconds) { |
| const min = Math.floor(seconds / 60); |
| const sec = Math.floor(seconds % 60).toString().padStart(2, '0'); |
| return `${min}:${sec}`; |
| } |
| |
| |
| init(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|