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 - WebSocket Streaming</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; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| color: white; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 20px; | |
| padding: 20px; | |
| } | |
| .avatar-section { | |
| position: relative; | |
| width: 400px; | |
| height: 400px; | |
| border-radius: 24px; | |
| overflow: hidden; | |
| background: #0f0f1a; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.5); | |
| } | |
| #idle-video, #ws-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| border-radius: 24px; | |
| } | |
| #idle-video { z-index: 5; } | |
| #ws-canvas { | |
| z-index: 10; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| #ws-canvas.active { opacity: 1; } | |
| .status-bar { | |
| position: absolute; | |
| top: 12px; | |
| left: 12px; | |
| display: flex; | |
| gap: 12px; | |
| z-index: 20; | |
| } | |
| .status-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: #666; | |
| display: inline-block; | |
| margin-right: 4px; | |
| } | |
| .status-dot.connected { background: #4ade80; } | |
| .status-dot.streaming { background: #60a5fa; animation: pulse 1s infinite; } | |
| .status-dot.error { background: #f87171; } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| #record-btn { | |
| width: 80px; | |
| height: 80px; | |
| border-radius: 50%; | |
| border: none; | |
| background: linear-gradient(145deg, #ef4444, #dc2626); | |
| color: white; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| box-shadow: 0 8px 20px rgba(239, 68, 68, 0.4); | |
| transition: all 0.2s; | |
| } | |
| #record-btn:hover { transform: scale(1.05); } | |
| #record-btn:active, #record-btn.recording { | |
| background: linear-gradient(145deg, #dc2626, #b91c1c); | |
| transform: scale(0.95); | |
| } | |
| #record-btn:disabled { | |
| background: #666; | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| } | |
| .response-text { | |
| text-align: center; | |
| font-size: 16px; | |
| color: #94a3b8; | |
| max-width: 400px; | |
| min-height: 48px; | |
| } | |
| .stats { | |
| display: flex; | |
| gap: 24px; | |
| font-size: 14px; | |
| color: #64748b; | |
| } | |
| .stats span { font-weight: 600; color: #94a3b8; } | |
| .mode-badge { | |
| background: #22c55e; | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 12px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <div class="mode-badge">WebSocket Mode (Proxy-friendly)</div> | |
| <div class="avatar-section"> | |
| <video id="idle-video" autoplay loop muted playsinline preload="auto" src="avatar_videos/idle.mp4"></video> | |
| <canvas id="ws-canvas"></canvas> | |
| <div class="status-bar"> | |
| <div><span class="status-dot" id="ws-status"></span>WS</div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <button id="record-btn" disabled>Conectando...</button> | |
| <p class="response-text" id="response-text">Iniciando...</p> | |
| </div> | |
| <div class="stats"> | |
| <div>Latência: <span id="latency">-</span></div> | |
| <div>Frames: <span id="frame-count">0</span></div> | |
| <div>FPS: <span id="fps">-</span></div> | |
| </div> | |
| </div> | |
| <audio id="audio-player" preload="none"></audio> | |
| <script> | |
| // Elements | |
| const idleVideo = document.getElementById('idle-video'); | |
| const wsCanvas = document.getElementById('ws-canvas'); | |
| const ctx = wsCanvas.getContext('2d'); | |
| const recordBtn = document.getElementById('record-btn'); | |
| const responseText = document.getElementById('response-text'); | |
| const wsStatus = document.getElementById('ws-status'); | |
| const latencyEl = document.getElementById('latency'); | |
| const frameCountEl = document.getElementById('frame-count'); | |
| const fpsEl = document.getElementById('fps'); | |
| const audioPlayer = document.getElementById('audio-player'); | |
| // State | |
| let ws = null; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| let frameCount = 0; | |
| let startTime = 0; | |
| let frameImage = new Image(); | |
| // Demo mode | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const isDemoMode = urlParams.get('demo') === 'true'; | |
| let demoMetrics = {}; | |
| // Connect WebSocket | |
| function connect() { | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsPath = window.location.pathname.replace(/\/[^\/]*$/, '') + '/ws/stream'; | |
| const wsUrl = `${protocol}//${window.location.host}${wsPath}`; | |
| console.log('[WS] Connecting to:', wsUrl); | |
| responseText.textContent = 'Conectando...'; | |
| ws = new WebSocket(wsUrl); | |
| ws.onopen = () => { | |
| console.log('[WS] Connected'); | |
| wsStatus.className = 'status-dot connected'; | |
| recordBtn.disabled = false; | |
| recordBtn.textContent = 'Falar'; | |
| responseText.textContent = 'Pressione e fale'; | |
| // Request WebSocket video mode | |
| ws.send(JSON.stringify({ type: 'config', ws_video: true })); | |
| }; | |
| ws.onclose = () => { | |
| console.log('[WS] Disconnected'); | |
| wsStatus.className = 'status-dot error'; | |
| recordBtn.disabled = true; | |
| responseText.textContent = 'Desconectado - recarregue'; | |
| setTimeout(connect, 3000); | |
| }; | |
| ws.onerror = (e) => { | |
| console.error('[WS] Error:', e); | |
| wsStatus.className = 'status-dot error'; | |
| }; | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| handleMessage(data); | |
| }; | |
| // Ping to keep alive | |
| setInterval(() => { | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| ws.send(JSON.stringify({ type: 'ping' })); | |
| } | |
| }, 10000); | |
| } | |
| function handleMessage(data) { | |
| switch (data.type) { | |
| case 'status': | |
| responseText.textContent = data.message; | |
| break; | |
| case 'transcription': | |
| responseText.textContent = `"${data.text}"`; | |
| break; | |
| case 'response': | |
| responseText.textContent = data.text; | |
| break; | |
| case 'audio': | |
| const basePath = window.location.pathname.replace(/\/[^\/]*$/, ''); | |
| audioPlayer.src = basePath + data.url; | |
| audioPlayer.load(); | |
| break; | |
| case 'info': | |
| frameCount = 0; | |
| startTime = Date.now(); | |
| frameCountEl.textContent = `0/${data.total_frames}`; | |
| wsCanvas.classList.add('active'); | |
| audioPlayer.play().catch(e => console.log('[Audio] Autoplay blocked')); | |
| if (isDemoMode) { | |
| demoMetrics.firstFrameInfoTime = Date.now(); | |
| demoMetrics.expectedFrames = data.total_frames; | |
| } | |
| break; | |
| case 'frame': | |
| case 'video_frame': | |
| frameImage.onload = function() { | |
| if (wsCanvas.width !== frameImage.width) { | |
| wsCanvas.width = frameImage.width; | |
| wsCanvas.height = frameImage.height; | |
| } | |
| ctx.drawImage(frameImage, 0, 0); | |
| }; | |
| frameImage.src = 'data:image/jpeg;base64,' + data.frame; | |
| frameCount++; | |
| frameCountEl.textContent = `${frameCount}/${data.total || '?'}`; | |
| // Calculate FPS | |
| const elapsed = (Date.now() - startTime) / 1000; | |
| if (elapsed > 0) { | |
| fpsEl.textContent = (frameCount / elapsed).toFixed(1); | |
| } | |
| if (isDemoMode && frameCount === 1) { | |
| demoMetrics.firstVideoFrameTime = Date.now(); | |
| console.log('[DEMO] First frame at', demoMetrics.firstVideoFrameTime - demoMetrics.audioSentTime, 'ms'); | |
| } | |
| break; | |
| case 'done': | |
| const totalTime = Date.now() - startTime; | |
| latencyEl.textContent = `${totalTime}ms`; | |
| frameCountEl.textContent = data.total_frames; | |
| setTimeout(() => { | |
| wsCanvas.classList.remove('active'); | |
| }, 1000); | |
| if (isDemoMode) { | |
| demoMetrics.responseCompleteTime = Date.now(); | |
| demoMetrics.totalFrames = data.total_frames; | |
| demoMetrics.serverMetrics = data.metrics; | |
| exportDemoMetrics({ success: true }); | |
| } | |
| break; | |
| case 'pong': | |
| break; | |
| case 'error': | |
| console.error('[WS] Server error:', data.message); | |
| responseText.textContent = 'Erro: ' + data.message; | |
| break; | |
| } | |
| } | |
| // Recording | |
| async function startRecording() { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = (e) => { | |
| if (e.data.size > 0) audioChunks.push(e.data); | |
| }; | |
| mediaRecorder.onstop = async () => { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| const base64 = reader.result.split(',')[1]; | |
| if (isDemoMode) { | |
| demoMetrics.audioSentTime = Date.now(); | |
| } | |
| ws.send(JSON.stringify({ | |
| type: 'start', | |
| audio_base64: base64, | |
| ws_video: true, // Request WebSocket video | |
| resolution: 256, | |
| batch_size: 16 | |
| })); | |
| responseText.textContent = 'Processando...'; | |
| wsStatus.className = 'status-dot streaming'; | |
| }; | |
| reader.readAsDataURL(audioBlob); | |
| stream.getTracks().forEach(t => t.stop()); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| recordBtn.classList.add('recording'); | |
| recordBtn.textContent = 'Soltar'; | |
| responseText.textContent = 'Gravando...'; | |
| } catch (e) { | |
| console.error('[Mic] Error:', e); | |
| responseText.textContent = 'Erro: permita acesso ao microfone'; | |
| } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && isRecording) { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| recordBtn.classList.remove('recording'); | |
| recordBtn.textContent = 'Falar'; | |
| } | |
| } | |
| // Demo mode: send audio file automatically | |
| async function runDemoMode() { | |
| console.log('[DEMO] Starting demo mode...'); | |
| demoMetrics = { pageLoadTime: Date.now() }; | |
| await new Promise(r => setTimeout(r, 2000)); | |
| try { | |
| const basePath = window.location.pathname.replace(/\/[^\/]*$/, ''); | |
| const response = await fetch(basePath + '/static/demo_audio.webm'); | |
| const audioBlob = await response.blob(); | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| const base64 = reader.result.split(',')[1]; | |
| demoMetrics.audioSentTime = Date.now(); | |
| ws.send(JSON.stringify({ | |
| type: 'start', | |
| audio_base64: base64, | |
| ws_video: true, | |
| resolution: 256, | |
| batch_size: 16 | |
| })); | |
| responseText.textContent = 'Demo: processando...'; | |
| wsStatus.className = 'status-dot streaming'; | |
| }; | |
| reader.readAsDataURL(audioBlob); | |
| } catch (e) { | |
| console.error('[DEMO] Error:', e); | |
| exportDemoMetrics({ success: false, error: e.message }); | |
| } | |
| } | |
| function exportDemoMetrics(result) { | |
| const metrics = { ...demoMetrics, ...result }; | |
| console.log('[DEMO] Final metrics:', JSON.stringify(metrics)); | |
| window.DEMO_METRICS = metrics; | |
| window.DEMO_COMPLETE = true; | |
| } | |
| // Event listeners | |
| recordBtn.addEventListener('mousedown', startRecording); | |
| recordBtn.addEventListener('mouseup', stopRecording); | |
| recordBtn.addEventListener('mouseleave', () => { if (isRecording) stopRecording(); }); | |
| recordBtn.addEventListener('touchstart', (e) => { e.preventDefault(); startRecording(); }); | |
| recordBtn.addEventListener('touchend', (e) => { e.preventDefault(); stopRecording(); }); | |
| // Initialize | |
| connect(); | |
| if (isDemoMode) { | |
| setTimeout(runDemoMode, 1000); | |
| } | |
| </script> | |
| </body> | |
| </html> | |