Spaces:
Running
Running
Download index.html from jason1966/Voxtral-TTS-MLX: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/jason1966/Voxtral-TTS-MLX/resolve/main/index.html
- Command line
-
hf download hf://spaces/jason1966/Voxtral-TTS-MLX/index.html
-
curl -L -o index.html https://huggingface.co/spaces/jason1966/Voxtral-TTS-MLX/resolve/main/index.html
19.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Voxtral TTS | MLX</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Press Start 2P', cursive; | |
| background: #5c94fc; | |
| min-height: 100vh; | |
| padding: 20px; | |
| image-rendering: pixelated; | |
| background-image: | |
| repeating-linear-gradient(0deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 4px), | |
| repeating-linear-gradient(90deg, transparent, transparent 3px, rgba(255,255,255,.05) 3px, rgba(255,255,255,.05) 4px); | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| background: linear-gradient(180deg, #fbd000 0%, #f48b00 100%); | |
| color: #fff; | |
| padding: 30px; | |
| text-align: center; | |
| border: 6px solid #000; | |
| box-shadow: 8px 8px 0 #000; | |
| margin-bottom: 30px; | |
| position: relative; | |
| } | |
| .header::before { | |
| content: ''; | |
| position: absolute; | |
| top: -3px; | |
| left: -3px; | |
| right: -3px; | |
| bottom: -3px; | |
| border: 3px solid #fff; | |
| pointer-events: none; | |
| } | |
| .header h1 { | |
| font-size: 1.8rem; | |
| margin-bottom: 15px; | |
| text-shadow: 4px 4px 0 #000; | |
| line-height: 1.6; | |
| } | |
| .header p { | |
| font-size: 0.6rem; | |
| text-shadow: 2px 2px 0 #000; | |
| line-height: 1.8; | |
| } | |
| .main { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 30px; | |
| } | |
| @media (max-width: 968px) { | |
| .main { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .panel { | |
| background: #fff; | |
| border: 6px solid #000; | |
| box-shadow: 8px 8px 0 #000; | |
| padding: 25px; | |
| position: relative; | |
| } | |
| .panel::before { | |
| content: ''; | |
| position: absolute; | |
| top: -3px; | |
| left: -3px; | |
| right: -3px; | |
| bottom: -3px; | |
| border: 3px solid #fbd000; | |
| pointer-events: none; | |
| } | |
| .coin-box { | |
| display: inline-block; | |
| background: #fbd000; | |
| border: 4px solid #000; | |
| padding: 8px 15px; | |
| margin-bottom: 15px; | |
| box-shadow: 4px 4px 0 #000; | |
| font-size: 0.65rem; | |
| } | |
| label { | |
| display: block; | |
| font-size: 0.7rem; | |
| margin-bottom: 10px; | |
| margin-top: 20px; | |
| color: #000; | |
| } | |
| select, textarea { | |
| width: 100%; | |
| padding: 12px; | |
| border: 4px solid #000; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 0.6rem; | |
| background: #fff; | |
| box-shadow: 4px 4px 0 #000; | |
| } | |
| select:focus, textarea:focus { | |
| outline: none; | |
| box-shadow: 0 0 0 4px #fbd000, 4px 4px 0 #000; | |
| } | |
| textarea { | |
| resize: vertical; | |
| line-height: 1.8; | |
| } | |
| .slider-group { | |
| margin-top: 20px; | |
| } | |
| .slider-group label { | |
| font-size: 0.55rem; | |
| margin-bottom: 8px; | |
| } | |
| .slider-value { | |
| display: inline-block; | |
| background: #fbd000; | |
| border: 3px solid #000; | |
| padding: 3px 8px; | |
| margin-left: 8px; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| height: 30px; | |
| background: #fff; | |
| border: 4px solid #000; | |
| box-shadow: inset 0 0 0 2px #000; | |
| outline: none; | |
| -webkit-appearance: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 25px; | |
| height: 25px; | |
| background: #f00; | |
| border: 3px solid #000; | |
| cursor: pointer; | |
| box-shadow: 2px 2px 0 #000; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 25px; | |
| height: 25px; | |
| background: #f00; | |
| border: 3px solid #000; | |
| cursor: pointer; | |
| box-shadow: 2px 2px 0 #000; | |
| } | |
| button { | |
| background: linear-gradient(180deg, #4caf50 0%, #2e7d32 100%); | |
| color: #fff; | |
| border: 5px solid #000; | |
| padding: 18px 30px; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| box-shadow: 6px 6px 0 #000; | |
| transition: all 0.1s; | |
| margin-top: 20px; | |
| width: 100%; | |
| text-shadow: 2px 2px 0 #000; | |
| } | |
| button:hover { | |
| transform: translate(2px, 2px); | |
| box-shadow: 4px 4px 0 #000; | |
| } | |
| button:active { | |
| transform: translate(4px, 4px); | |
| box-shadow: 2px 2px 0 #000; | |
| } | |
| button:disabled { | |
| background: #999; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .download-btn { | |
| background: linear-gradient(180deg, #2196f3 0%, #1565c0 100%); | |
| padding: 14px 20px; | |
| font-size: 0.65rem; | |
| margin-top: 15px; | |
| } | |
| .download-btn:hover { | |
| background: linear-gradient(180deg, #42a5f5 0%, #1976d2 100%); | |
| } | |
| .download-btn:disabled { | |
| background: #999; | |
| } | |
| .audio-area { | |
| background: #000; | |
| border: 5px solid #000; | |
| padding: 25px; | |
| min-height: 120px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: inset 4px 4px 0 rgba(0,255,0,0.2); | |
| margin-top: 15px; | |
| } | |
| .audio-area.loading { | |
| flex-direction: column; | |
| } | |
| .audio-area audio { | |
| width: 100%; | |
| filter: sepia(20%) saturate(70%) hue-rotate(80deg); | |
| } | |
| .placeholder-text { | |
| color: #0f0; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 0.6rem; | |
| text-align: center; | |
| line-height: 1.8; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| position: relative; | |
| } | |
| .spinner::before, | |
| .spinner::after { | |
| content: ''; | |
| position: absolute; | |
| border: 8px solid #0f0; | |
| animation: spin 1s linear infinite; | |
| } | |
| .spinner::before { | |
| width: 40px; | |
| height: 40px; | |
| top: 10px; | |
| left: 10px; | |
| } | |
| .spinner::after { | |
| width: 20px; | |
| height: 20px; | |
| top: 20px; | |
| left: 20px; | |
| animation-direction: reverse; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| color: #0f0; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 0.5rem; | |
| margin-top: 15px; | |
| animation: blink 1s step-end infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| .stats { | |
| margin-top: 15px; | |
| padding: 15px; | |
| background: #fbd000; | |
| border: 4px solid #000; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 0.5rem; | |
| color: #000; | |
| line-height: 1.8; | |
| box-shadow: 4px 4px 0 #000; | |
| } | |
| .error-box { | |
| background: #f44336; | |
| color: #fff; | |
| border: 4px solid #000; | |
| padding: 15px; | |
| margin-top: 15px; | |
| font-size: 0.55rem; | |
| line-height: 1.8; | |
| box-shadow: 4px 4px 0 #000; | |
| } | |
| .char-count { | |
| font-size: 0.45rem; | |
| color: #666; | |
| margin-top: 8px; | |
| text-align: right; | |
| } | |
| .footer { | |
| text-align: center; | |
| padding: 30px 20px; | |
| margin-top: 30px; | |
| background: #fbd000; | |
| border: 6px solid #000; | |
| box-shadow: 8px 8px 0 #000; | |
| font-size: 0.5rem; | |
| line-height: 2; | |
| } | |
| .footer a { | |
| color: #000; | |
| text-decoration: none; | |
| border-bottom: 2px solid #000; | |
| } | |
| .footer a:hover { | |
| background: #000; | |
| color: #fbd000; | |
| } | |
| .pixel-icon { | |
| display: inline-block; | |
| font-size: 1.5rem; | |
| animation: float 2s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| ::-webkit-scrollbar { | |
| width: 16px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #ddd; | |
| border: 3px solid #000; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #fbd000; | |
| border: 3px solid #000; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #f48b00; | |
| } | |
| .service-notice { | |
| background: #ff4444; | |
| color: #fff; | |
| border: 6px solid #000; | |
| box-shadow: 8px 8px 0 #000; | |
| padding: 25px; | |
| text-align: center; | |
| margin-bottom: 30px; | |
| position: relative; | |
| } | |
| .service-notice::before { | |
| content: ''; | |
| position: absolute; | |
| top: -3px; | |
| left: -3px; | |
| right: -3px; | |
| bottom: -3px; | |
| border: 3px solid #fff; | |
| pointer-events: none; | |
| } | |
| .service-notice h2 { | |
| font-size: 1.2rem; | |
| margin-bottom: 15px; | |
| text-shadow: 3px 3px 0 #000; | |
| } | |
| .service-notice p { | |
| font-size: 0.6rem; | |
| line-height: 2; | |
| text-shadow: 2px 2px 0 #000; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="service-notice"> | |
| <h2>โ ๏ธ SERVICE TEMPORARILY UNAVAILABLE โ ๏ธ</h2> | |
| <p>THIS SERVICE IS CURRENTLY OFFLINE</p> | |
| <p>BACKEND SERVER IS NOT RUNNING</p> | |
| </div> | |
| <div class="header"> | |
| <h1><span class="pixel-icon">๐</span> VOXTRAL TTS <span class="pixel-icon">๐</span></h1> | |
| <p>APPLE SILICON POWERED โข MLX ACCELERATION</p> | |
| <p>20 VOICES โข 9 LANGUAGES</p> | |
| </div> | |
| <div class="main"> | |
| <div class="panel"> | |
| <div class="coin-box">โ๏ธ TEXT INPUT</div> | |
| <label for="textInput">๐ ENTER TEXT TO SPEAK</label> | |
| <textarea id="textInput" rows="8" placeholder="Type or paste your text here...">Hello! This is a demo of Voxtral text-to-speech running on Apple Silicon with MLX acceleration.</textarea> | |
| <div class="char-count" id="charCount">89 CHARS</div> | |
| <label for="voiceSelect">๐ญ VOICE</label> | |
| <select id="voiceSelect"> | |
| <optgroup label="๐ฌ๐ง English"> | |
| <option value="casual_male" selected>Casual Male</option> | |
| <option value="casual_female">Casual Female</option> | |
| <option value="cheerful_female">Cheerful Female</option> | |
| <option value="neutral_male">Neutral Male</option> | |
| <option value="neutral_female">Neutral Female</option> | |
| </optgroup> | |
| <optgroup label="๐ซ๐ท French"> | |
| <option value="fr_male">French Male</option> | |
| <option value="fr_female">French Female</option> | |
| </optgroup> | |
| <optgroup label="๐ช๐ธ Spanish"> | |
| <option value="es_male">Spanish Male</option> | |
| <option value="es_female">Spanish Female</option> | |
| </optgroup> | |
| <optgroup label="๐ฉ๐ช German"> | |
| <option value="de_male">German Male</option> | |
| <option value="de_female">German Female</option> | |
| </optgroup> | |
| <optgroup label="๐ฎ๐น Italian"> | |
| <option value="it_male">Italian Male</option> | |
| <option value="it_female">Italian Female</option> | |
| </optgroup> | |
| <optgroup label="๐ต๐น Portuguese"> | |
| <option value="pt_male">Portuguese Male</option> | |
| <option value="pt_female">Portuguese Female</option> | |
| </optgroup> | |
| <optgroup label="๐ณ๐ฑ Dutch"> | |
| <option value="nl_male">Dutch Male</option> | |
| <option value="nl_female">Dutch Female</option> | |
| </optgroup> | |
| <optgroup label="๐ธ๐ฆ Arabic"> | |
| <option value="ar_male">Arabic Male</option> | |
| </optgroup> | |
| <optgroup label="๐ฎ๐ณ Hindi"> | |
| <option value="hi_male">Hindi Male</option> | |
| <option value="hi_female">Hindi Female</option> | |
| </optgroup> | |
| </select> | |
| <div class="slider-group"> | |
| <label>โก SPEED <span class="slider-value" id="speedValue">1.0</span></label> | |
| <input type="range" id="speed" min="0.5" max="2.0" step="0.1" value="1.0"> | |
| </div> | |
| <button id="submitBtn" onclick="generateSpeech()">๐ GENERATE SPEECH!</button> | |
| </div> | |
| <div class="panel"> | |
| <div class="coin-box">๐ AUDIO OUTPUT</div> | |
| <div class="audio-area" id="audioArea"> | |
| <div class="placeholder-text" id="placeholderText">ENTER TEXT AND PRESS<br>GENERATE SPEECH!</div> | |
| </div> | |
| <div id="statsArea"></div> | |
| <div id="errorArea"></div> | |
| <button class="download-btn" id="downloadBtn" onclick="downloadAudio()" disabled>๐พ DOWNLOAD WAV</button> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <div>๐ฎ POWERED BY MLX ON APPLE SILICON ๐</div> | |
| <div>MODEL: <a href="https://huggingface.co/mlx-community/Voxtral-4B-TTS-2603-mlx-4bit" target="_blank">VOXTRAL-4B-TTS-MLX-4BIT</a></div> | |
| <div>BASE: <a href="https://huggingface.co/mistralai/Voxtral-4B-TTS-2603" target="_blank">MISTRALAI/VOXTRAL-4B-TTS-2603</a></div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_URL = 'https://voxtraltts.locoremind.com'; | |
| let audioBlob = null; | |
| let audioObjectUrl = null; | |
| // Character count | |
| const textInput = document.getElementById('textInput'); | |
| const charCount = document.getElementById('charCount'); | |
| textInput.oninput = () => { | |
| charCount.textContent = `${textInput.value.length} CHARS`; | |
| }; | |
| // Speed slider | |
| document.getElementById('speed').oninput = (e) => { | |
| document.getElementById('speedValue').textContent = e.target.value; | |
| }; | |
| // Generate speech | |
| async function generateSpeech() { | |
| const text = textInput.value.trim(); | |
| if (!text) { | |
| alert('PLEASE ENTER SOME TEXT FIRST!'); | |
| return; | |
| } | |
| const voice = document.getElementById('voiceSelect').value; | |
| const speed = parseFloat(document.getElementById('speed').value); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const audioArea = document.getElementById('audioArea'); | |
| const statsArea = document.getElementById('statsArea'); | |
| const errorArea = document.getElementById('errorArea'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| // Reset | |
| submitBtn.disabled = true; | |
| downloadBtn.disabled = true; | |
| statsArea.innerHTML = ''; | |
| errorArea.innerHTML = ''; | |
| // Clean up previous audio | |
| if (audioObjectUrl) { | |
| URL.revokeObjectURL(audioObjectUrl); | |
| audioObjectUrl = null; | |
| } | |
| // Show loading | |
| audioArea.className = 'audio-area loading'; | |
| audioArea.innerHTML = '<div class="spinner"></div><div class="loading-text">GENERATING SPEECH...</div>'; | |
| try { | |
| const startTime = Date.now(); | |
| const response = await fetch(`${API_URL}/v1/audio/speech`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| input: text, | |
| voice: voice, | |
| speed: speed | |
| }) | |
| }); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`HTTP ${response.status}: ${errorText}`); | |
| } | |
| audioBlob = await response.blob(); | |
| const endTime = Date.now(); | |
| const duration = ((endTime - startTime) / 1000).toFixed(2); | |
| // Create audio player | |
| audioObjectUrl = URL.createObjectURL(audioBlob); | |
| audioArea.className = 'audio-area'; | |
| audioArea.innerHTML = `<audio controls autoplay src="${audioObjectUrl}" style="width:100%"></audio>`; | |
| // Show stats | |
| const sizeKB = (audioBlob.size / 1024).toFixed(1); | |
| statsArea.innerHTML = `<div class="stats">โฑ ${duration}s | ๐ฆ ${sizeKB} KB | ๐ญ ${voice} | โก ${speed}x</div>`; | |
| // Enable download | |
| downloadBtn.disabled = false; | |
| } catch (error) { | |
| audioArea.className = 'audio-area'; | |
| audioArea.innerHTML = '<div class="placeholder-text">GENERATION FAILED</div>'; | |
| errorArea.innerHTML = `<div class="error-box">ERROR: ${error.message}</div>`; | |
| audioBlob = null; | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| } | |
| // Download audio | |
| function downloadAudio() { | |
| if (!audioBlob) return; | |
| const voice = document.getElementById('voiceSelect').value; | |
| const url = URL.createObjectURL(audioBlob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `voxtral_tts_${voice}.wav`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Init char count | |
| charCount.textContent = `${textInput.value.length} CHARS`; | |
| </script> | |
| </body> | |
| </html> | |