Diffusers
MuseTalk1.5 / server /static /index_streaming.html
Marcos
Add complete MuseTalk real-time avatar system with WebSocket streaming
97ff05d
Raw
History Blame Contribute Delete
24.9 kB
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MuseTalk - Streaming em Tempo Real</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 50%, #16213e 100%);
min-height: 100vh;
color: #fff;
}
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
header {
text-align: center;
padding: 15px 0;
margin-bottom: 20px;
}
header h1 {
font-size: 1.5rem;
background: linear-gradient(90deg, #00ff88, #00d4ff, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 5px;
}
.subtitle {
color: #666;
font-size: 0.85rem;
}
.live-badge {
display: inline-block;
background: #ff4444;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: bold;
margin-left: 10px;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.video-section {
background: rgba(255,255,255,0.03);
border-radius: 24px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid rgba(255,255,255,0.08);
}
.canvas-container {
position: relative;
width: 100%;
aspect-ratio: 1;
max-width: 450px;
margin: 0 auto 15px;
border-radius: 20px;
overflow: hidden;
background: #000;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
#videoCanvas {
width: 100%;
height: 100%;
object-fit: cover;
}
.canvas-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 15px;
background: rgba(0,0,0,0.7);
opacity: 0;
transition: opacity 0.3s;
}
.canvas-overlay.active { opacity: 1; }
.stats-bar {
position: absolute;
bottom: 0;
left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
padding: 20px 15px 10px;
display: flex;
justify-content: space-between;
font-size: 0.75rem;
}
.stat {
display: flex;
align-items: center;
gap: 5px;
color: #aaa;
}
.stat-value {
color: #00ff88;
font-weight: 600;
}
/* Listening ring */
.listening-ring {
position: absolute;
top: -3px; left: -3px; right: -3px; bottom: -3px;
border: 3px solid #00ff88;
border-radius: 23px;
display: none;
animation: ring-pulse 0.6s ease-in-out infinite;
}
.canvas-container.listening .listening-ring { display: block; }
@keyframes ring-pulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 1; transform: scale(1.005); }
}
.status-text {
text-align: center;
font-size: 1rem;
color: #ccc;
min-height: 50px;
padding: 10px;
line-height: 1.4;
}
.progress-bar {
width: 100%;
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88, #00d4ff);
width: 0%;
transition: width 0.1s;
}
.timing-info {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
font-size: 0.7rem;
color: #666;
}
.timing-info span {
padding: 4px 10px;
background: rgba(255,255,255,0.05);
border-radius: 20px;
}
.chat-section {
background: rgba(255,255,255,0.03);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(255,255,255,0.08);
}
.chat-history {
max-height: 120px;
overflow-y: auto;
margin-bottom: 15px;
}
.message {
margin-bottom: 8px;
padding: 10px 14px;
border-radius: 14px;
max-width: 85%;
font-size: 0.85rem;
line-height: 1.4;
}
.message.user {
background: linear-gradient(135deg, #7c3aed, #5b21b6);
margin-left: auto;
border-bottom-right-radius: 4px;
}
.message.assistant {
background: rgba(255,255,255,0.08);
border-bottom-left-radius: 4px;
}
.controls {
display: flex;
gap: 10px;
}
.record-btn {
flex: 1;
padding: 18px;
border: none;
border-radius: 16px;
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, #00ff88, #00cc6a);
color: #000;
}
.record-btn.recording {
background: linear-gradient(135deg, #ff4444, #cc0000);
color: white;
animation: rec-pulse 0.4s ease-in-out infinite;
}
@keyframes rec-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.record-btn:disabled {
background: #333;
color: #666;
cursor: not-allowed;
}
.settings-btn {
padding: 18px;
border: none;
border-radius: 16px;
background: rgba(255,255,255,0.08);
color: #fff;
cursor: pointer;
transition: all 0.2s;
}
.settings-btn:hover {
background: rgba(255,255,255,0.12);
}
/* Settings Modal */
.modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.9);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal.active { display: flex; }
.modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #252547 100%);
padding: 30px;
border-radius: 24px;
border: 1px solid rgba(124,58,237,0.3);
min-width: 320px;
}
.modal-content h3 {
margin-bottom: 25px;
font-size: 1.2rem;
}
.setting-group {
margin-bottom: 20px;
}
.setting-group label {
display: block;
margin-bottom: 8px;
color: #888;
font-size: 0.85rem;
}
.setting-group select {
width: 100%;
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.08);
color: #fff;
font-size: 1rem;
}
.setting-group select option {
background: #1a1a2e;
}
.modal-close {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #7c3aed, #5b21b6);
border: none;
border-radius: 12px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.connection-status {
position: fixed;
top: 10px;
right: 10px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.7rem;
font-weight: 600;
}
.connection-status.connected {
background: rgba(0,255,136,0.2);
color: #00ff88;
}
.connection-status.disconnected {
background: rgba(255,68,68,0.2);
color: #ff4444;
}
/* Hidden audio element */
#audioPlayer { display: none; }
</style>
</head>
<body>
<div class="connection-status disconnected" id="connection-status">Desconectado</div>
<div class="container">
<header>
<h1>MuseTalk Streaming <span class="live-badge">LIVE</span></h1>
<p class="subtitle">Video gerado frame-a-frame em tempo real via WebSocket</p>
</header>
<div class="video-section">
<div class="canvas-container" id="canvas-container">
<div class="listening-ring"></div>
<canvas id="videoCanvas" width="512" height="512"></canvas>
<div class="canvas-overlay" id="canvas-overlay">
<div style="font-size: 3rem;">🎙️</div>
<div>Aguardando...</div>
</div>
<div class="stats-bar" id="stats-bar">
<div class="stat">Frame: <span class="stat-value" id="frame-count">0/0</span></div>
<div class="stat">FPS: <span class="stat-value" id="fps-display">0</span></div>
<div class="stat">Latencia: <span class="stat-value" id="latency-display">0ms</span></div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="status-text" id="status-text">
Pressione o botao e fale para iniciar o streaming em tempo real
</div>
<div class="timing-info" id="timing-info"></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" viewBox="0 0 24 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="settings-btn" id="settings-btn">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Settings Modal -->
<div class="modal" id="settings-modal">
<div class="modal-content">
<h3>Configuracoes de Streaming</h3>
<div class="setting-group">
<label>Resolucao</label>
<select id="resolution-select">
<option value="128">128px - Ultra rapido</option>
<option value="192">192px - Rapido</option>
<option value="256" selected>256px - Padrao</option>
</select>
</div>
<div class="setting-group">
<label>Batch Size</label>
<select id="batch-select">
<option value="4">4 - Menor latencia</option>
<option value="8" selected>8 - Balanceado</option>
<option value="16">16 - Mais rapido</option>
</select>
</div>
<button class="modal-close" id="modal-close">Salvar</button>
</div>
</div>
<audio id="audioPlayer"></audio>
<script>
// Elements
const canvas = document.getElementById('videoCanvas');
const ctx = canvas.getContext('2d');
const canvasContainer = document.getElementById('canvas-container');
const canvasOverlay = document.getElementById('canvas-overlay');
const statusText = document.getElementById('status-text');
const progressFill = document.getElementById('progress-fill');
const frameCount = document.getElementById('frame-count');
const fpsDisplay = document.getElementById('fps-display');
const latencyDisplay = document.getElementById('latency-display');
const timingInfo = document.getElementById('timing-info');
const chatHistory = document.getElementById('chat-history');
const recordBtn = document.getElementById('record-btn');
const btnText = document.getElementById('btn-text');
const settingsBtn = document.getElementById('settings-btn');
const settingsModal = document.getElementById('settings-modal');
const modalClose = document.getElementById('modal-close');
const resolutionSelect = document.getElementById('resolution-select');
const batchSelect = document.getElementById('batch-select');
const connectionStatus = document.getElementById('connection-status');
const audioPlayer = document.getElementById('audioPlayer');
let ws = null;
let mediaRecorder = null;
let audioChunks = [];
let isRecording = false;
let frameStartTime = 0;
let framesReceived = 0;
let totalFrames = 0;
let targetFps = 30;
let audioStarted = false;
// Load settings
if (localStorage.getItem('stream_resolution')) {
resolutionSelect.value = localStorage.getItem('stream_resolution');
}
if (localStorage.getItem('stream_batch')) {
batchSelect.value = localStorage.getItem('stream_batch');
}
// Settings modal
settingsBtn.onclick = () => settingsModal.classList.add('active');
modalClose.onclick = () => {
settingsModal.classList.remove('active');
localStorage.setItem('stream_resolution', resolutionSelect.value);
localStorage.setItem('stream_batch', batchSelect.value);
};
// Connect WebSocket
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws/stream`);
ws.onopen = () => {
console.log('WebSocket connected');
connectionStatus.textContent = 'Conectado';
connectionStatus.className = 'connection-status connected';
};
ws.onclose = () => {
console.log('WebSocket disconnected');
connectionStatus.textContent = 'Desconectado';
connectionStatus.className = 'connection-status disconnected';
// Reconnect after 2 seconds
setTimeout(connectWebSocket, 2000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
}
function handleMessage(data) {
switch (data.type) {
case 'status':
statusText.textContent = data.message;
break;
case 'transcription':
addMessage('user', data.text);
break;
case 'response':
addMessage('assistant', data.text);
statusText.textContent = data.text;
break;
case 'audio':
// Preload audio
audioPlayer.src = data.url;
audioPlayer.load();
break;
case 'info':
totalFrames = data.total_frames;
targetFps = data.fps;
frameCount.textContent = `0/${totalFrames}`;
frameStartTime = Date.now();
framesReceived = 0;
audioStarted = false;
canvasOverlay.classList.remove('active');
break;
case 'frame':
renderFrame(data.frame, data.index);
framesReceived++;
// Start audio on first frame
if (!audioStarted && framesReceived === 1) {
audioPlayer.play().catch(e => console.log('Audio autoplay blocked'));
audioStarted = true;
}
// Update stats
frameCount.textContent = `${data.index + 1}/${totalFrames}`;
const progress = ((data.index + 1) / totalFrames) * 100;
progressFill.style.width = `${progress}%`;
// Calculate FPS
const elapsed = (Date.now() - frameStartTime) / 1000;
const currentFps = framesReceived / elapsed;
fpsDisplay.textContent = currentFps.toFixed(1);
// Estimate latency (frames behind real-time)
const expectedFrame = elapsed * targetFps;
const latencyFrames = expectedFrame - framesReceived;
const latencyMs = (latencyFrames / targetFps) * 1000;
latencyDisplay.textContent = `${Math.max(0, latencyMs).toFixed(0)}ms`;
break;
case 'done':
statusText.textContent = `Streaming completo! ${data.total_frames} frames`;
progressFill.style.width = '100%';
recordBtn.disabled = false;
timingInfo.innerHTML = `<span>Total: ${((Date.now() - frameStartTime) / 1000).toFixed(1)}s</span>`;
break;
case 'error':
statusText.textContent = `Erro: ${data.message}`;
canvasOverlay.classList.add('active');
recordBtn.disabled = false;
break;
}
}
function renderFrame(base64Data, index) {
const img = new Image();
img.onload = () => {
// Clear and draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Calculate scaling to fit canvas while maintaining aspect ratio
const scale = Math.min(canvas.width / img.width, canvas.height / img.height);
const x = (canvas.width - img.width * scale) / 2;
const y = (canvas.height - img.height * scale) / 2;
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
};
img.src = 'data:image/jpeg;base64,' + base64Data;
}
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 initMicrophone() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => {
audioChunks.push(e.data);
};
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
audioChunks = [];
await sendAudio(audioBlob);
};
statusText.textContent = 'Pronto! Pressione e fale para streaming em tempo real.';
} catch (err) {
statusText.textContent = 'Erro: Permita acesso ao microfone';
recordBtn.disabled = true;
}
}
async function sendAudio(audioBlob) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
statusText.textContent = 'Erro: WebSocket nao conectado';
recordBtn.disabled = false;
return;
}
// Convert blob to base64
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
// Send to WebSocket
ws.send(JSON.stringify({
type: 'start',
audio_base64: base64,
resolution: parseInt(resolutionSelect.value),
batch_size: parseInt(batchSelect.value)
}));
frameStartTime = Date.now();
};
reader.readAsDataURL(audioBlob);
}
function startRecording() {
if (isRecording || !mediaRecorder || recordBtn.disabled) return;
if (!ws || ws.readyState !== WebSocket.OPEN) {
statusText.textContent = 'Aguarde conexao WebSocket...';
return;
}
isRecording = true;
audioChunks = [];
mediaRecorder.start();
recordBtn.classList.replace('idle', 'recording');
btnText.textContent = 'Gravando... solte para enviar';
canvasContainer.classList.add('listening');
canvasOverlay.innerHTML = '<div style="font-size: 3rem;">🎤</div><div>Ouvindo...</div>';
canvasOverlay.classList.add('active');
}
function stopRecording() {
if (!isRecording) return;
isRecording = false;
mediaRecorder.stop();
recordBtn.classList.replace('recording', 'idle');
btnText.textContent = 'Pressione para falar';
canvasContainer.classList.remove('listening');
recordBtn.disabled = true;
canvasOverlay.innerHTML = '<div class="spinner" style="width:50px;height:50px;border:4px solid rgba(255,255,255,0.2);border-top-color:#00ff88;border-radius:50%;animation:spin 0.8s linear infinite;"></div><div>Processando...</div>';
statusText.textContent = 'Enviando audio...';
}
// 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(); });
// Draw initial frame (placeholder)
function drawPlaceholder() {
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#333';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText('Aguardando streaming...', canvas.width/2, canvas.height/2);
}
// Initialize
drawPlaceholder();
connectWebSocket();
initMicrophone();
</script>
</body>
</html>