Diffusers
MuseTalk1.5 / server /static /index_ws.html
Marcos
Add complete MuseTalk real-time avatar system with WebSocket streaming
97ff05d
Raw
History Blame Contribute Delete
15.4 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 - 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>