Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hodi TTS - Ewe Text to Speech</title> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --text-color: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .container { | |
| background-color: var(--card-bg); | |
| border-radius: 24px; | |
| padding: 40px; | |
| width: 100%; | |
| max-width: 600px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 8px; | |
| font-size: 2.5rem; | |
| background: linear-gradient(135deg, #a5b4fc 0%, #6366f1 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: 800; | |
| } | |
| p.subtitle { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| margin-bottom: 32px; | |
| font-size: 1.1rem; | |
| } | |
| .input-group { | |
| margin-bottom: 24px; | |
| } | |
| textarea { | |
| width: 100%; | |
| height: 150px; | |
| padding: 16px; | |
| background-color: rgba(0, 0, 0, 0.2); | |
| border: 2px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; | |
| color: var(--text-color); | |
| font-size: 1rem; | |
| resize: vertical; | |
| transition: border-color 0.3s ease; | |
| box-sizing: border-box; | |
| font-family: inherit; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--primary-color); | |
| } | |
| button { | |
| width: 100%; | |
| padding: 16px; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| button:hover { | |
| background-color: var(--primary-hover); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button:disabled { | |
| opacity: 0.7; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .audio-container { | |
| margin-top: 32px; | |
| padding: 24px; | |
| background-color: rgba(255, 255, 255, 0.03); | |
| border-radius: 16px; | |
| display: none; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| audio { | |
| width: 100%; | |
| height: 40px; | |
| margin-top: 12px; | |
| } | |
| /* Custom Audio Player Styling Placeholder (Standard controls are used for reliability) */ | |
| audio::-webkit-media-controls-panel { | |
| background-color: #334155; | |
| } | |
| audio::-webkit-media-controls-current-time-display, | |
| audio::-webkit-media-controls-time-remaining-display { | |
| color: #e2e8f0; | |
| } | |
| .status-message { | |
| text-align: center; | |
| margin-top: 16px; | |
| min-height: 24px; | |
| font-weight: 500; | |
| } | |
| .loading { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid rgba(255, 255, 255, .3); | |
| border-radius: 50%; | |
| border-top-color: #fff; | |
| animation: spin 1s ease-in-out infinite; | |
| margin-right: 10px; | |
| vertical-align: middle; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .error { | |
| color: #ef4444; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Hodi Voice</h1> | |
| <p class="subtitle">Ewe & Mina Text-to-Speech Generator</p> | |
| <div class="input-group"> | |
| <select id="languageSelect" | |
| style="width: 100%; padding: 12px; margin-bottom: 16px; background-color: rgba(0,0,0,0.2); border: 2px solid rgba(255,255,255,0.1); border-radius: 12px; color: var(--text-color); font-size: 1rem;"> | |
| <option value="ewe">Ewe</option> | |
| <option value="mina">Mina</option> | |
| </select> | |
| <textarea id="textInput" placeholder="Enter text here to synthesize..."></textarea> | |
| </div> | |
| <button id="generateBtn" onclick="generateSpeech()"> | |
| <span id="btnText">Generate Speech</span> | |
| </button> | |
| <div id="statusMessage" class="status-message"></div> | |
| <div class="audio-container" id="audioContainer"> | |
| <h3 style="margin: 0 0 12px 0; font-size: 1rem; color: var(--text-secondary);">Generated Audio</h3> | |
| <audio id="audioPlayer" controls></audio> | |
| </div> | |
| </div> | |
| <script> | |
| async function generateSpeech() { | |
| const text = document.getElementById('textInput').value.trim(); | |
| const language = document.getElementById('languageSelect').value; | |
| const btn = document.getElementById('generateBtn'); | |
| const btnText = document.getElementById('btnText'); | |
| const statusDiv = document.getElementById('statusMessage'); | |
| const audioContainer = document.getElementById('audioContainer'); | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| if (!text) { | |
| statusDiv.innerHTML = '<span class="error">Please enter some text.</span>'; | |
| return; | |
| } | |
| // Reset UI | |
| statusDiv.innerHTML = '<div class="loading"></div> Generating audio...'; | |
| statusDiv.className = 'status-message'; | |
| btn.disabled = true; | |
| audioContainer.style.display = 'none'; | |
| try { | |
| const response = await fetch('http://localhost:5000/tts', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| text: text, | |
| language: language | |
| }), | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json(); | |
| throw new Error(errorData.error || 'Failed to generate speech'); | |
| } | |
| const blob = await response.blob(); | |
| const audioUrl = URL.createObjectURL(blob); | |
| audioPlayer.src = audioUrl; | |
| audioContainer.style.display = 'block'; | |
| statusDiv.innerHTML = ''; | |
| // Auto play | |
| try { | |
| await audioPlayer.play(); | |
| } catch (e) { | |
| console.log("Auto-play prevented by browser policy"); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| statusDiv.innerHTML = `<span class="error">Error: ${error.message}</span>`; | |
| } finally { | |
| btn.disabled = false; | |
| btnText.textContent = 'Generate Speech'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |