Instructions to use marcosremar2/MuseTalk1.5 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use marcosremar2/MuseTalk1.5 with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("marcosremar2/MuseTalk1.5", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
| <html lang="pt-BR"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MuseTalk - Avatar Speech-to-Speech</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
| min-height: 100vh; | |
| color: #fff; | |
| } | |
| .container { max-width: 900px; margin: 0 auto; padding: 20px; } | |
| header { text-align: center; padding: 20px 0; } | |
| header h1 { | |
| font-size: 1.8rem; | |
| background: linear-gradient(90deg, #00d4ff, #7c3aed); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| header p { color: #888; font-size: 0.85rem; margin-top: 5px; } | |
| .main-content { display: flex; flex-direction: column; gap: 20px; } | |
| .avatar-section { | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 16px; | |
| padding: 20px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .avatar-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .avatar-image { | |
| width: 120px; | |
| height: 120px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #7c3aed, #00d4ff); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 3rem; | |
| flex-shrink: 0; | |
| } | |
| .avatar-image.speaking { | |
| animation: speaking-pulse 0.5s infinite; | |
| } | |
| @keyframes speaking-pulse { | |
| 0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.5); } | |
| 50% { transform: scale(1.05); box-shadow: 0 0 20px 10px rgba(124, 58, 237, 0.3); } | |
| } | |
| .response-area { flex: 1; } | |
| .response-text { | |
| font-size: 1.1rem; | |
| line-height: 1.6; | |
| color: #e0e0e0; | |
| min-height: 60px; | |
| } | |
| .response-text.typing::after { | |
| content: '|'; | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } | |
| .status-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-top: 15px; | |
| padding: 8px 12px; | |
| background: rgba(0,0,0,0.3); | |
| border-radius: 8px; | |
| font-size: 0.8rem; | |
| } | |
| .status-dot { | |
| width: 8px; height: 8px; | |
| border-radius: 50%; | |
| background: #444; | |
| } | |
| .status-dot.active { background: #00ff88; animation: pulse 1s infinite; } | |
| .status-dot.processing { background: #ffaa00; animation: pulse 0.3s infinite; } | |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } | |
| .chat-section { | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 16px; | |
| padding: 20px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .chat-history { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| margin-bottom: 15px; | |
| } | |
| .message { | |
| margin-bottom: 10px; | |
| padding: 10px 14px; | |
| border-radius: 12px; | |
| max-width: 80%; | |
| font-size: 0.9rem; | |
| } | |
| .message.user { | |
| background: linear-gradient(135deg, #7c3aed, #5b21b6); | |
| margin-left: auto; | |
| } | |
| .message.assistant { | |
| background: rgba(255,255,255,0.1); | |
| } | |
| .controls { display: flex; gap: 10px; } | |
| .record-btn { | |
| flex: 1; | |
| padding: 16px; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| } | |
| .record-btn.idle { | |
| background: linear-gradient(135deg, #00d4ff, #0099cc); | |
| color: white; | |
| } | |
| .record-btn.recording { | |
| background: linear-gradient(135deg, #ff4444, #cc0000); | |
| color: white; | |
| } | |
| .record-btn:disabled { | |
| background: #444; | |
| cursor: not-allowed; | |
| } | |
| .record-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| } | |
| .clear-btn { | |
| padding: 16px; | |
| border: none; | |
| border-radius: 12px; | |
| background: rgba(255,255,255,0.1); | |
| color: #fff; | |
| cursor: pointer; | |
| } | |
| .timing-info { | |
| margin-top: 10px; | |
| font-size: 0.7rem; | |
| color: #666; | |
| text-align: center; | |
| } | |
| .timing-info span { | |
| margin: 0 5px; | |
| padding: 2px 6px; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 4px; | |
| } | |
| audio { display: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>MuseTalk Avatar</h1> | |
| <p>Converse com o avatar usando sua voz - resposta em ~2s</p> | |
| </header> | |
| <div class="main-content"> | |
| <div class="avatar-section"> | |
| <div class="avatar-container"> | |
| <div class="avatar-image" id="avatar"> | |
| <span>🤖</span> | |
| </div> | |
| <div class="response-area"> | |
| <div class="response-text" id="response-text"> | |
| Ola! Pressione o botao e fale comigo. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="status-bar"> | |
| <div class="status-dot" id="status-dot"></div> | |
| <span id="status-text">Pronto</span> | |
| </div> | |
| </div> | |
| <div class="chat-section"> | |
| <div class="chat-history" id="chat-history"></div> | |
| <div class="controls"> | |
| <button class="record-btn idle" id="record-btn"> | |
| <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/> | |
| <path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/> | |
| </svg> | |
| <span id="btn-text">Pressione para falar</span> | |
| </button> | |
| <button class="clear-btn" id="clear-btn" title="Limpar"> | |
| <svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="timing-info" id="timing-info"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <audio id="audio-player"></audio> | |
| <script> | |
| const recordBtn = document.getElementById('record-btn'); | |
| const btnText = document.getElementById('btn-text'); | |
| const chatHistory = document.getElementById('chat-history'); | |
| const avatar = document.getElementById('avatar'); | |
| const responseText = document.getElementById('response-text'); | |
| const statusDot = document.getElementById('status-dot'); | |
| const statusText = document.getElementById('status-text'); | |
| const timingInfo = document.getElementById('timing-info'); | |
| const audioPlayer = document.getElementById('audio-player'); | |
| const clearBtn = document.getElementById('clear-btn'); | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| async function init() { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| mediaRecorder.ondataavailable = e => audioChunks.push(e.data); | |
| mediaRecorder.onstop = () => processAudio(); | |
| setStatus('active', 'Pronto para conversar'); | |
| } catch (err) { | |
| setStatus('error', 'Erro: permita acesso ao microfone'); | |
| recordBtn.disabled = true; | |
| } | |
| } | |
| function setStatus(state, text) { | |
| statusDot.className = 'status-dot ' + state; | |
| statusText.textContent = text; | |
| } | |
| function addMessage(role, text) { | |
| const div = document.createElement('div'); | |
| div.className = 'message ' + role; | |
| div.textContent = text; | |
| chatHistory.appendChild(div); | |
| chatHistory.scrollTop = chatHistory.scrollHeight; | |
| } | |
| async function processAudio() { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| audioChunks = []; | |
| setStatus('processing', 'Transcrevendo...'); | |
| responseText.textContent = '...'; | |
| responseText.classList.add('typing'); | |
| recordBtn.disabled = true; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('audio', audioBlob, 'recording.webm'); | |
| const response = await fetch('/api/conversation', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) throw new Error('Erro na requisicao'); | |
| const result = await response.json(); | |
| // Show user message | |
| addMessage('user', result.user_text); | |
| // Show assistant response immediately | |
| setStatus('processing', 'Reproduzindo resposta...'); | |
| responseText.classList.remove('typing'); | |
| responseText.textContent = result.assistant_text; | |
| addMessage('assistant', result.assistant_text); | |
| // Play audio | |
| avatar.classList.add('speaking'); | |
| audioPlayer.src = result.audio_url; | |
| audioPlayer.play(); | |
| audioPlayer.onended = () => { | |
| avatar.classList.remove('speaking'); | |
| setStatus('active', 'Pronto'); | |
| }; | |
| // Show timing | |
| const t = result.timing; | |
| timingInfo.innerHTML = ` | |
| <span>STT: ${t.stt}s</span> | |
| <span>LLM: ${t.llm}s</span> | |
| <span>TTS: ${t.tts}s</span> | |
| <span><strong>Total: ${t.total}s</strong></span> | |
| <span style="color:#ffaa00">Video: gerando...</span> | |
| `; | |
| // Poll for video status | |
| if (result.job_id) { | |
| pollVideoStatus(result.job_id); | |
| } | |
| } catch (err) { | |
| setStatus('error', 'Erro: ' + err.message); | |
| responseText.classList.remove('typing'); | |
| responseText.textContent = 'Desculpe, ocorreu um erro. Tente novamente.'; | |
| } finally { | |
| recordBtn.disabled = false; | |
| } | |
| } | |
| function startRecording() { | |
| if (isRecording || !mediaRecorder || recordBtn.disabled) return; | |
| isRecording = true; | |
| audioChunks = []; | |
| mediaRecorder.start(); | |
| recordBtn.classList.replace('idle', 'recording'); | |
| btnText.textContent = 'Gravando... solte para enviar'; | |
| setStatus('processing', 'Gravando...'); | |
| } | |
| function stopRecording() { | |
| if (!isRecording) return; | |
| isRecording = false; | |
| mediaRecorder.stop(); | |
| recordBtn.classList.replace('recording', 'idle'); | |
| btnText.textContent = 'Pressione para falar'; | |
| } | |
| recordBtn.addEventListener('mousedown', startRecording); | |
| recordBtn.addEventListener('mouseup', stopRecording); | |
| recordBtn.addEventListener('mouseleave', stopRecording); | |
| recordBtn.addEventListener('touchstart', e => { e.preventDefault(); startRecording(); }); | |
| recordBtn.addEventListener('touchend', e => { e.preventDefault(); stopRecording(); }); | |
| clearBtn.addEventListener('click', async () => { | |
| await fetch('/api/clear-history', { method: 'POST' }); | |
| chatHistory.innerHTML = ''; | |
| responseText.textContent = 'Conversa limpa! Fale comigo.'; | |
| timingInfo.innerHTML = ''; | |
| }); | |
| async function pollVideoStatus(jobId) { | |
| const startTime = Date.now(); | |
| const poll = async () => { | |
| try { | |
| const response = await fetch(`/api/video-status/${jobId}`); | |
| const data = await response.json(); | |
| if (data.status === 'completed') { | |
| const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); | |
| // Update timing to show video is ready | |
| const videoLink = document.createElement('a'); | |
| videoLink.href = `/api/video-job/${jobId}`; | |
| videoLink.target = '_blank'; | |
| videoLink.style.color = '#00ff88'; | |
| videoLink.textContent = `Video: ${elapsed}s ✓`; | |
| // Replace the "gerando" text | |
| const spans = timingInfo.querySelectorAll('span'); | |
| const videoSpan = Array.from(spans).find(s => s.textContent.includes('Video:')); | |
| if (videoSpan) { | |
| videoSpan.innerHTML = ''; | |
| videoSpan.appendChild(videoLink); | |
| } | |
| } else if (data.status === 'error') { | |
| const spans = timingInfo.querySelectorAll('span'); | |
| const videoSpan = Array.from(spans).find(s => s.textContent.includes('Video:')); | |
| if (videoSpan) { | |
| videoSpan.style.color = '#ff4444'; | |
| videoSpan.textContent = 'Video: erro'; | |
| } | |
| } else { | |
| // Still generating, poll again | |
| setTimeout(poll, 2000); | |
| } | |
| } catch (e) { | |
| console.error('Error polling video status:', e); | |
| } | |
| }; | |
| poll(); | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |