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 Fast - Avatar com Vídeo</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, #0f0f1a 0%, #1a1a2e 100%); | |
| min-height: 100vh; | |
| color: #fff; | |
| } | |
| .container { max-width: 1000px; margin: 0 auto; padding: 15px; } | |
| header { text-align: center; padding: 15px 0; } | |
| header h1 { | |
| font-size: 1.5rem; | |
| background: linear-gradient(90deg, #00d4ff, #7c3aed, #ff6b6b); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| header p { color: #666; font-size: 0.8rem; margin-top: 5px; } | |
| .main-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| } | |
| @media (max-width: 768px) { | |
| .main-grid { grid-template-columns: 1fr; } | |
| } | |
| .panel { | |
| background: rgba(255,255,255,0.03); | |
| border-radius: 16px; | |
| padding: 20px; | |
| border: 1px solid rgba(255,255,255,0.08); | |
| } | |
| .video-panel { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .video-container { | |
| position: relative; | |
| width: 100%; | |
| aspect-ratio: 1; | |
| background: #000; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| margin-bottom: 15px; | |
| } | |
| #avatar-video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .video-overlay { | |
| position: absolute; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: rgba(0,0,0,0.7); | |
| transition: opacity 0.3s; | |
| } | |
| .video-overlay.hidden { opacity: 0; pointer-events: none; } | |
| .avatar-placeholder { | |
| width: 100px; | |
| height: 100px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #7c3aed, #00d4ff); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 3rem; | |
| margin-bottom: 15px; | |
| } | |
| .avatar-placeholder.speaking { | |
| animation: pulse 0.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| } | |
| .progress-bar { | |
| width: 80%; | |
| height: 6px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 3px; | |
| margin-top: 15px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #00d4ff, #7c3aed); | |
| width: 0%; | |
| transition: width 0.3s; | |
| } | |
| .response-box { | |
| background: rgba(0,0,0,0.3); | |
| border-radius: 10px; | |
| padding: 15px; | |
| min-height: 80px; | |
| } | |
| .response-text { | |
| font-size: 1rem; | |
| line-height: 1.5; | |
| color: #e0e0e0; | |
| } | |
| .chat-panel { | |
| display: flex; | |
| flex-direction: column; | |
| max-height: 500px; | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| margin-bottom: 15px; | |
| padding-right: 10px; | |
| } | |
| .message { | |
| margin-bottom: 10px; | |
| padding: 10px 14px; | |
| border-radius: 12px; | |
| max-width: 85%; | |
| font-size: 0.85rem; | |
| animation: slideIn 0.2s ease; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .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: 14px; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .record-btn.idle { | |
| background: linear-gradient(135deg, #00d4ff, #0099cc); | |
| color: white; | |
| } | |
| .record-btn.recording { | |
| background: linear-gradient(135deg, #ff4444, #cc0000); | |
| color: white; | |
| animation: recording 1s infinite; | |
| } | |
| @keyframes recording { | |
| 0%, 100% { box-shadow: 0 0 0 0 rgba(255,68,68,0.5); } | |
| 50% { box-shadow: 0 0 20px 5px rgba(255,68,68,0.3); } | |
| } | |
| .record-btn:disabled { | |
| background: #333; | |
| cursor: not-allowed; | |
| } | |
| .icon-btn { | |
| padding: 14px; | |
| border: none; | |
| border-radius: 12px; | |
| background: rgba(255,255,255,0.1); | |
| color: #fff; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .icon-btn:hover { background: rgba(255,255,255,0.2); } | |
| .status-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 10px 15px; | |
| background: rgba(0,0,0,0.3); | |
| border-radius: 8px; | |
| margin-top: 15px; | |
| font-size: 0.75rem; | |
| } | |
| .status-left { display: flex; align-items: center; gap: 8px; } | |
| .status-dot { | |
| width: 8px; height: 8px; | |
| border-radius: 50%; | |
| background: #444; | |
| } | |
| .status-dot.ready { background: #00ff88; } | |
| .status-dot.processing { background: #ffaa00; animation: blink 0.5s infinite; } | |
| .status-dot.error { background: #ff4444; } | |
| @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } | |
| .timing-info { | |
| color: #666; | |
| font-size: 0.7rem; | |
| } | |
| .timing-info span { | |
| margin-left: 10px; | |
| padding: 2px 6px; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 4px; | |
| } | |
| .mode-toggle { | |
| display: flex; | |
| gap: 5px; | |
| margin-bottom: 15px; | |
| } | |
| .mode-btn { | |
| flex: 1; | |
| padding: 8px; | |
| border: none; | |
| border-radius: 8px; | |
| background: rgba(255,255,255,0.05); | |
| color: #888; | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .mode-btn.active { | |
| background: rgba(124,58,237,0.3); | |
| color: #fff; | |
| } | |
| audio { display: none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>MuseTalk Fast Avatar</h1> | |
| <p>Speech-to-Speech com Video Lip-Sync Otimizado</p> | |
| </header> | |
| <div class="main-grid"> | |
| <div class="panel video-panel"> | |
| <div class="mode-toggle"> | |
| <button class="mode-btn active" id="mode-audio">Apenas Áudio (~2s)</button> | |
| <button class="mode-btn" id="mode-video">Com Vídeo (~30-60s)</button> | |
| </div> | |
| <div class="video-container"> | |
| <video id="avatar-video" playsinline></video> | |
| <div class="video-overlay" id="video-overlay"> | |
| <div class="avatar-placeholder" id="avatar-placeholder">🤖</div> | |
| <div id="overlay-status">Pronto para conversar</div> | |
| <div class="progress-bar" id="progress-bar" style="display:none;"> | |
| <div class="progress-fill" id="progress-fill"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="response-box"> | |
| <div class="response-text" id="response-text"> | |
| Olá! Pressione o botão e fale comigo. Escolha o modo acima. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel chat-panel"> | |
| <div class="chat-history" id="chat-history"></div> | |
| <div class="controls"> | |
| <button class="record-btn idle" id="record-btn"> | |
| <svg width="20" height="20" 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">Segure para falar</span> | |
| </button> | |
| <button class="icon-btn" id="clear-btn" title="Limpar conversa"> | |
| <svg width="20" height="20" 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="status-bar"> | |
| <div class="status-left"> | |
| <div class="status-dot ready" id="status-dot"></div> | |
| <span id="status-text">Pronto</span> | |
| </div> | |
| <div class="timing-info" id="timing-info"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <audio id="audio-player"></audio> | |
| <script> | |
| // Elements | |
| const recordBtn = document.getElementById('record-btn'); | |
| const btnText = document.getElementById('btn-text'); | |
| const chatHistory = document.getElementById('chat-history'); | |
| const avatarVideo = document.getElementById('avatar-video'); | |
| const videoOverlay = document.getElementById('video-overlay'); | |
| const avatarPlaceholder = document.getElementById('avatar-placeholder'); | |
| const overlayStatus = document.getElementById('overlay-status'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressFill = document.getElementById('progress-fill'); | |
| 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'); | |
| const modeAudio = document.getElementById('mode-audio'); | |
| const modeVideo = document.getElementById('mode-video'); | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| let videoMode = false; // false = audio only, true = with video | |
| // Mode toggle | |
| modeAudio.onclick = () => { | |
| videoMode = false; | |
| modeAudio.classList.add('active'); | |
| modeVideo.classList.remove('active'); | |
| }; | |
| modeVideo.onclick = () => { | |
| videoMode = true; | |
| modeVideo.classList.add('active'); | |
| modeAudio.classList.remove('active'); | |
| }; | |
| 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('ready', 'Pronto'); | |
| } catch (err) { | |
| setStatus('error', 'Erro: permita 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; | |
| } | |
| function showProgress(percent, message) { | |
| progressBar.style.display = 'block'; | |
| progressFill.style.width = percent + '%'; | |
| overlayStatus.textContent = message || `Gerando vídeo: ${percent}%`; | |
| } | |
| function hideProgress() { | |
| progressBar.style.display = 'none'; | |
| progressFill.style.width = '0%'; | |
| } | |
| async function processAudio() { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| audioChunks = []; | |
| recordBtn.disabled = true; | |
| setStatus('processing', 'Processando...'); | |
| responseText.textContent = '...'; | |
| const formData = new FormData(); | |
| formData.append('audio', audioBlob, 'recording.webm'); | |
| try { | |
| if (videoMode) { | |
| await processWithVideo(formData); | |
| } else { | |
| await processAudioOnly(formData); | |
| } | |
| } catch (err) { | |
| setStatus('error', 'Erro: ' + err.message); | |
| responseText.textContent = 'Erro ao processar. Tente novamente.'; | |
| hideProgress(); | |
| } finally { | |
| recordBtn.disabled = false; | |
| } | |
| } | |
| async function processAudioOnly(formData) { | |
| overlayStatus.textContent = 'Processando...'; | |
| const response = await fetch('/api/conversation', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) throw new Error('Erro na requisição'); | |
| const result = await response.json(); | |
| // Show messages | |
| addMessage('user', result.user_text); | |
| addMessage('assistant', result.assistant_text); | |
| responseText.textContent = result.assistant_text; | |
| // Play audio | |
| overlayStatus.textContent = 'Reproduzindo...'; | |
| avatarPlaceholder.classList.add('speaking'); | |
| audioPlayer.src = result.audio_url; | |
| await audioPlayer.play(); | |
| audioPlayer.onended = () => { | |
| avatarPlaceholder.classList.remove('speaking'); | |
| overlayStatus.textContent = 'Pronto'; | |
| setStatus('ready', '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>Total: ${t.total}s</span>`; | |
| setStatus('ready', 'Reproduzindo áudio'); | |
| } | |
| async function processWithVideo(formData) { | |
| showProgress(5, 'Enviando áudio...'); | |
| const response = await fetch('/api/conversation', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) throw new Error('Erro na requisição'); | |
| const result = await response.json(); | |
| // Show messages immediately | |
| addMessage('user', result.user_text); | |
| addMessage('assistant', result.assistant_text); | |
| responseText.textContent = result.assistant_text; | |
| const t = result.timing; | |
| timingInfo.innerHTML = `<span>STT: ${t.stt}s</span><span>LLM: ${t.llm}s</span><span>TTS: ${t.tts}s</span>`; | |
| // Start playing audio while video generates | |
| avatarPlaceholder.classList.add('speaking'); | |
| audioPlayer.src = result.audio_url; | |
| // Poll for video status | |
| showProgress(10, 'Gerando vídeo...'); | |
| const jobId = result.job_id; | |
| const pollVideo = async () => { | |
| try { | |
| const statusRes = await fetch(`/api/video-status/${jobId}`); | |
| const status = await statusRes.json(); | |
| if (status.status === 'completed') { | |
| // Video ready - play it | |
| showProgress(100, 'Vídeo pronto!'); | |
| setTimeout(() => { | |
| videoOverlay.classList.add('hidden'); | |
| avatarVideo.src = `/api/video/${jobId}`; | |
| avatarVideo.play(); | |
| hideProgress(); | |
| setStatus('ready', 'Reproduzindo vídeo'); | |
| avatarVideo.onended = () => { | |
| videoOverlay.classList.remove('hidden'); | |
| overlayStatus.textContent = 'Pronto'; | |
| avatarPlaceholder.classList.remove('speaking'); | |
| }; | |
| // Update timing | |
| timingInfo.innerHTML += `<span>Vídeo: pronto</span>`; | |
| }, 500); | |
| } else if (status.status === 'error') { | |
| throw new Error(status.error || 'Erro ao gerar vídeo'); | |
| } else { | |
| showProgress(status.progress || 10, status.message || 'Gerando...'); | |
| setTimeout(pollVideo, 1000); | |
| } | |
| } catch (e) { | |
| console.error('Poll error:', e); | |
| hideProgress(); | |
| overlayStatus.textContent = 'Erro no vídeo, usando áudio'; | |
| } | |
| }; | |
| // Play audio immediately, poll video in background | |
| audioPlayer.play(); | |
| audioPlayer.onended = () => { | |
| if (!avatarVideo.src) { | |
| avatarPlaceholder.classList.remove('speaking'); | |
| } | |
| }; | |
| pollVideo(); | |
| } | |
| function startRecording() { | |
| if (isRecording || !mediaRecorder || recordBtn.disabled) return; | |
| isRecording = true; | |
| audioChunks = []; | |
| mediaRecorder.start(); | |
| recordBtn.classList.replace('idle', 'recording'); | |
| btnText.textContent = 'Solte para enviar'; | |
| setStatus('processing', 'Gravando...'); | |
| overlayStatus.textContent = 'Gravando...'; | |
| } | |
| function stopRecording() { | |
| if (!isRecording) return; | |
| isRecording = false; | |
| mediaRecorder.stop(); | |
| recordBtn.classList.replace('recording', 'idle'); | |
| btnText.textContent = 'Segure 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!'; | |
| timingInfo.innerHTML = ''; | |
| avatarVideo.src = ''; | |
| videoOverlay.classList.remove('hidden'); | |
| overlayStatus.textContent = 'Pronto'; | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |