Files

1118 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepFaceLab Linux Web UI</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<!-- FontAwesome for Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg-base: #0a0b10;
--bg-surface: #121420;
--bg-surface-elevated: #1a1d2f;
--border-color: #24293e;
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--primary: #06b6d4; /* Cyber Cyan */
--primary-glow: rgba(6, 182, 212, 0.15);
--secondary: #8b5cf6; /* Laser Violet */
--secondary-glow: rgba(139, 92, 246, 0.15);
--success: #10b981; /* Emerald Green */
--danger: #ef4444; /* Crimson Red */
--warning: #f59e0b; /* Amber */
--terminal-bg: #050508;
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', sans-serif;
font-weight: 600;
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-base);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary);
}
/* Glassmorphism Header */
header {
background: rgba(18, 20, 32, 0.75);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.header-title-container {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-logo {
font-size: 1.75rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-family: 'Outfit', sans-serif;
font-weight: 700;
letter-spacing: -0.5px;
}
.header-status-badge {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
padding: 0.35rem 0.75rem;
border-radius: 50px;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-secondary);
}
.status-dot.active {
background: var(--success);
box-shadow: 0 0 10px var(--success);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
/* Layout Grid */
.app-container {
flex: 1;
display: grid;
grid-template-columns: 320px 1fr;
max-width: 1800px;
margin: 0 auto;
width: 100%;
gap: 1.5rem;
padding: 1.5rem;
}
@media (max-width: 1024px) {
.app-container {
grid-template-columns: 1fr;
}
}
/* Sidebar Navigation & Steps */
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 1.25rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
transition: var(--transition-smooth);
}
.card:hover {
border-color: rgba(6, 182, 212, 0.3);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.card-title {
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-primary);
}
/* Step List (Timeline-style) */
.step-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.step-item {
cursor: pointer;
padding: 0.75rem 1rem;
border-radius: 10px;
background: rgba(255,255,255,0.02);
border: 1px solid transparent;
transition: var(--transition-smooth);
display: flex;
align-items: center;
justify-content: space-between;
}
.step-item:hover {
background: rgba(6, 182, 212, 0.05);
border-color: rgba(6, 182, 212, 0.15);
transform: translateX(4px);
}
.step-item.active {
background: var(--primary-glow);
border-color: var(--primary);
color: var(--primary);
}
.step-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.step-number {
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255,255,255,0.05);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid var(--border-color);
}
.step-item.active .step-number {
background: var(--primary);
color: var(--bg-base);
border-color: var(--primary);
}
.step-name {
font-size: 0.85rem;
font-weight: 500;
}
/* Workspace Stats Card */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.stat-box {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 0.75rem;
text-align: center;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary);
font-family: 'Outfit', sans-serif;
margin-top: 0.25rem;
}
.stat-label {
font-size: 0.65rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Main Panel */
.main-content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Script Browser */
.script-browser-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1rem;
}
.script-card {
background: var(--bg-surface-elevated);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 0.75rem;
transition: var(--transition-smooth);
}
.script-card:hover {
border-color: var(--secondary);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.1);
}
.script-details {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.script-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-primary);
}
.script-desc {
font-size: 0.75rem;
color: var(--text-secondary);
line-height: 1.4;
}
.script-file {
font-family: 'Fira Code', monospace;
font-size: 0.65rem;
background: rgba(0,0,0,0.25);
padding: 0.2rem 0.4rem;
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.03);
width: fit-content;
color: var(--primary);
}
.btn-run {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: var(--text-primary);
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition-smooth);
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
}
.btn-run:hover {
opacity: 0.95;
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
/* Interactive Console / Terminal */
.terminal-card {
flex: 1;
min-height: 400px;
display: flex;
flex-direction: column;
background: var(--terminal-bg);
border: 1px solid #1f2937;
border-radius: 16px;
overflow: hidden;
box-shadow: inset 0 0 20px rgba(0,0,0,0.8);
}
.terminal-header {
background: #0f111a;
border-bottom: 1px solid #1f2937;
padding: 0.75rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-dots {
display: flex;
gap: 6px;
}
.terminal-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.terminal-dot.red { background: #ff5f56; }
.terminal-dot.yellow { background: #ffbd2e; }
.terminal-dot.green { background: #27c93f; }
.terminal-title {
font-family: 'Fira Code', monospace;
font-size: 0.75rem;
color: var(--text-secondary);
}
.terminal-controls {
display: flex;
gap: 0.5rem;
}
.btn-stop {
background: var(--danger);
color: white;
border: none;
padding: 0.35rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.35rem;
transition: var(--transition-smooth);
}
.btn-stop:hover {
background: #dc2626;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.4);
}
.btn-stop:disabled {
background: #374151;
color: #9ca3af;
cursor: not-allowed;
box-shadow: none;
}
.terminal-body {
flex: 1;
padding: 1.25rem;
overflow-y: auto;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.85rem;
color: #d1d5db;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.terminal-input-bar {
background: #0d0f17;
border-top: 1px solid #1f2937;
display: flex;
align-items: center;
padding: 0.5rem 1rem;
}
.terminal-prompt {
font-family: 'Fira Code', monospace;
color: var(--success);
margin-right: 0.5rem;
font-size: 0.85rem;
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
color: white;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
outline: none;
}
/* Model Training Live Preview Section */
.preview-card {
background: var(--bg-surface);
}
.preview-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
margin-top: 0.5rem;
}
.preview-img-container {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
background: rgba(0,0,0,0.3);
position: relative;
aspect-ratio: 1/1;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition-smooth);
}
.preview-img-container:hover {
border-color: var(--primary);
}
.preview-img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.preview-meta {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(4px);
padding: 0.5rem;
font-size: 0.65rem;
color: var(--text-primary);
text-align: center;
border-top: 1px solid rgba(255,255,255,0.05);
}
.no-preview-text {
color: var(--text-secondary);
font-size: 0.8rem;
text-align: center;
width: 100%;
padding: 2rem 0;
grid-column: 1 / -1;
}
/* Video Upload Section */
.upload-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 640px) {
.upload-section {
grid-template-columns: 1fr;
}
}
.upload-box {
border: 2px dashed var(--border-color);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
cursor: pointer;
background: rgba(255,255,255,0.01);
transition: var(--transition-smooth);
position: relative;
}
.upload-box:hover {
border-color: var(--primary);
background: rgba(6, 182, 212, 0.02);
}
.upload-box input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.upload-icon {
font-size: 1.75rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.upload-title {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.upload-desc {
font-size: 0.7rem;
color: var(--text-secondary);
}
.upload-progress {
margin-top: 0.75rem;
height: 4px;
background: var(--border-color);
border-radius: 2px;
overflow: hidden;
display: none;
}
.upload-progress-bar {
height: 100%;
background: var(--primary);
width: 0%;
transition: width 0.1s ease;
}
/* Monitor panels */
.monitor-panel {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.monitor-item {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.monitor-label {
color: var(--text-secondary);
}
.monitor-value {
font-weight: 600;
color: var(--text-primary);
}
</style>
</head>
<body>
<header>
<div class="header-title-container">
<span class="header-logo"><i class="fa-solid fa-brain"></i> DeepFaceLab</span>
<div class="header-status-badge">
<span id="status-dot" class="status-dot"></span>
<span id="status-text">Checking server...</span>
</div>
</div>
<div class="monitor-panel" id="monitor-panel">
<!-- Dynamic system info will render here -->
</div>
</header>
<div class="app-container">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fa-solid fa-route"></i> DFL Pipeline</h2>
</div>
<ul class="step-list">
<li class="step-item active" onclick="switchCategory('workspace', this)">
<div class="step-info">
<span class="step-number">0</span>
<span class="step-name">Workspace Setup</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('src_video', this)">
<div class="step-info">
<span class="step-number">1</span>
<span class="step-name">Source Video</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('dst_video', this)">
<div class="step-info">
<span class="step-number">2</span>
<span class="step-name">Destination Video</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('xseg', this)">
<div class="step-info">
<span class="step-number">3</span>
<span class="step-name">XSeg Labeled Masking</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('pretrain', this)">
<div class="step-info">
<span class="step-number">4</span>
<span class="step-name">Pretrain Weights</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('training', this)">
<div class="step-info">
<span class="step-number">5</span>
<span class="step-name">Model Training</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('merging', this)">
<div class="step-info">
<span class="step-number">6</span>
<span class="step-name">Face Merging</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
<li class="step-item" onclick="switchCategory('export', this)">
<div class="step-info">
<span class="step-number">7</span>
<span class="step-name">Render & Export</span>
</div>
<i class="fa-solid fa-chevron-right text-secondary"></i>
</li>
</ul>
</div>
<!-- Workspace Status -->
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fa-solid fa-database"></i> Workspace Files</h2>
</div>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-label">Src Frames</div>
<div class="stat-value" id="stat-src-frames">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Src Faces</div>
<div class="stat-value" id="stat-src-faces">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Dst Frames</div>
<div class="stat-value" id="stat-dst-frames">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Dst Faces</div>
<div class="stat-value" id="stat-dst-faces">0</div>
</div>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="main-content">
<!-- Upload Panel -->
<section class="upload-section">
<div class="upload-box card">
<div class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></div>
<div class="upload-title">Upload data_src.mp4</div>
<div class="upload-desc">Drag & drop or click to upload source video</div>
<input type="file" onchange="uploadFile(this, 'src')">
<div class="upload-progress" id="progress-src">
<div class="upload-progress-bar" id="progress-bar-src"></div>
</div>
</div>
<div class="upload-box card">
<div class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></div>
<div class="upload-title">Upload data_dst.mp4</div>
<div class="upload-desc">Drag & drop or click to upload destination video</div>
<input type="file" onchange="uploadFile(this, 'dst')">
<div class="upload-progress" id="progress-dst">
<div class="upload-progress-bar" id="progress-bar-dst"></div>
</div>
</div>
</section>
<!-- Script execution launcher -->
<section class="card" id="launcher-card">
<div class="card-header">
<h2 class="card-title" id="category-title">Workspace Setup</h2>
</div>
<div class="script-browser-grid" id="script-grid">
<!-- Dynamic scripts list -->
</div>
</section>
<!-- Interactive Console -->
<section class="terminal-card">
<div class="terminal-header">
<div class="terminal-dots">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
</div>
<div class="terminal-title" id="terminal-process-title">Process: None</div>
<div class="terminal-controls">
<button id="btn-stop" class="btn-stop" disabled onclick="stopScript()">
<i class="fa-solid fa-stop"></i> Kill Script
</button>
</div>
</div>
<div class="terminal-body" id="terminal-body">
Waiting for process launch...
</div>
<div class="terminal-input-bar">
<span class="terminal-prompt">$</span>
<input type="text" id="terminal-input" class="terminal-input" placeholder="Type prompt answer and press Enter..." onkeydown="handleTerminalInput(event)">
</div>
</section>
<!-- Live Model Previews -->
<section class="card preview-card">
<div class="card-header">
<h2 class="card-title"><i class="fa-solid fa-eye"></i> Model Previews & Outputs</h2>
<button onclick="fetchPreviews()" class="btn-run" style="width: auto; padding: 0.35rem 0.75rem;">
<i class="fa-solid fa-rotate"></i> Refresh Previews
</button>
</div>
<div class="preview-gallery" id="preview-gallery">
<div class="no-preview-text">No previews found. Start training to generate real-time previews.</div>
</div>
</section>
</main>
</div>
<script>
let allScripts = {};
let activeCategory = 'workspace';
let isRunning = false;
let lastLogs = "";
let logInterval = null;
let statsInterval = null;
// Fetch scripts list on start
window.addEventListener('DOMContentLoaded', () => {
fetch('/api/scripts')
.then(r => r.json())
.then(data => {
allScripts = data;
renderScripts();
});
// Poll stats and process details
updateSystemStatus();
statsInterval = setInterval(updateSystemStatus, 2000);
// Initial previews fetch
fetchPreviews();
setInterval(fetchPreviews, 10000); // refresh previews every 10s
});
function switchCategory(catName, el) {
// Update active state in UI list
document.querySelectorAll('.step-item').forEach(item => item.classList.remove('active'));
if(el) el.classList.add('active');
activeCategory = catName;
renderScripts();
}
function renderScripts() {
const grid = document.getElementById('script-grid');
const titleEl = document.getElementById('category-title');
grid.innerHTML = "";
if(!allScripts[activeCategory]) return;
const categoryData = allScripts[activeCategory];
titleEl.textContent = categoryData.title;
categoryData.scripts.forEach(script => {
const card = document.createElement('div');
card.className = "script-card";
card.innerHTML = `
<div class="script-details">
<span class="script-name">${script.name}</span>
<span class="script-desc">${script.desc}</span>
<span class="script-file">${script.file}</span>
</div>
<button class="btn-run" onclick="runScript('${script.file}')" ${isRunning ? 'disabled style="opacity:0.5; cursor:not-allowed;"' : ''}>
<i class="fa-solid fa-play"></i> Run
</button>
`;
grid.appendChild(card);
});
}
function runScript(scriptName) {
if(isRunning) return;
fetch('/api/run', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({script: scriptName})
})
.then(r => r.json())
.then(res => {
if (res.error) {
alert(res.error);
} else {
document.getElementById('terminal-body').textContent = `=== Launching ${scriptName} ===\n`;
document.getElementById('terminal-process-title').textContent = `Process: ${scriptName}`;
document.getElementById('btn-stop').disabled = false;
isRunning = true;
startLogPolling();
renderScripts();
}
})
.catch(err => {
console.error("Failed to run script:", err);
});
}
function stopScript() {
fetch('/api/stop', { method: 'POST' })
.then(r => r.json())
.then(res => {
if (res.error) {
alert(res.error);
}
});
}
function handleTerminalInput(e) {
if (e.key === 'Enter') {
const inputEl = document.getElementById('terminal-input');
const text = inputEl.value;
inputEl.value = "";
fetch('/api/input', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: text})
});
}
}
function startLogPolling() {
if(logInterval) clearInterval(logInterval);
logInterval = setInterval(() => {
fetch('/api/logs')
.then(r => r.json())
.then(data => {
const terminal = document.getElementById('terminal-body');
// Parse carriage returns (updates progress bars in place)
let parsedLogs = formatTerminalText(data.logs);
terminal.textContent = parsedLogs;
terminal.scrollTop = terminal.scrollHeight; // Auto scroll to bottom
});
}, 500);
}
function formatTerminalText(text) {
let lines = text.split('\n');
let outputLines = [];
for (let line of lines) {
if (line.includes('\r')) {
let parts = line.split('\r');
// Get the last non-empty part to simulate carriage return overwrite
line = parts[parts.length - 1] || parts[parts.length - 2] || "";
}
outputLines.push(line);
}
return outputLines.join('\n');
}
function updateSystemStatus() {
fetch('/api/status')
.then(r => r.json())
.then(res => {
// Update status badge
const dot = document.getElementById('status-dot');
const txt = document.getElementById('status-text');
if (res.running) {
dot.className = "status-dot active";
txt.textContent = `Running: ${res.script}`;
document.getElementById('btn-stop').disabled = false;
document.getElementById('terminal-process-title').textContent = `Process: ${res.script}`;
if (!isRunning) {
isRunning = true;
startLogPolling();
renderScripts();
}
} else {
dot.className = "status-dot";
txt.textContent = "System Idle";
document.getElementById('btn-stop').disabled = true;
document.getElementById('terminal-process-title').textContent = "Process: None";
if (isRunning) {
isRunning = false;
clearInterval(logInterval);
// Do a final log fetch
setTimeout(() => {
fetch('/api/logs')
.then(r => r.json())
.then(d => {
document.getElementById('terminal-body').textContent = formatTerminalText(d.logs) + "\n=== Process Completed ===";
document.getElementById('terminal-body').scrollTop = document.getElementById('terminal-body').scrollHeight;
});
}, 500);
renderScripts();
}
}
// Update system resource panel
const monitor = document.getElementById('monitor-panel');
monitor.innerHTML = "";
// CPU and Memory
monitor.innerHTML += `
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-microchip"></i> CPU</span>
<span class="monitor-value">${res.system.cpu}</span>
</div>
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-memory"></i> RAM</span>
<span class="monitor-value">${res.system.mem}</span>
</div>
`;
// GPU Stats
if (res.gpu && res.gpu.length > 0) {
res.gpu.forEach(g => {
monitor.innerHTML += `
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-bolt"></i> GPU</span>
<span class="monitor-value">${g.name} (${g.gpu_util})</span>
</div>
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-thermometer-half"></i> Temp</span>
<span class="monitor-value">${g.temp}</span>
</div>
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-hard-drive"></i> VRAM</span>
<span class="monitor-value">${g.mem_used} / ${g.mem_total}</span>
</div>
`;
});
} else {
monitor.innerHTML += `
<div class="monitor-item">
<span class="monitor-label"><i class="fa-solid fa-bolt"></i> GPU</span>
<span class="monitor-value">CPU Mode / No Nvidia GPU</span>
</div>
`;
}
// Update workspace stats
document.getElementById('stat-src-frames').textContent = res.workspace.src_frames;
document.getElementById('stat-src-faces').textContent = res.workspace.src_faces;
document.getElementById('stat-dst-frames').textContent = res.workspace.dst_frames;
document.getElementById('stat-dst-faces').textContent = res.workspace.dst_faces;
});
}
function fetchPreviews() {
fetch('/api/preview')
.then(r => r.json())
.then(previews => {
const gallery = document.getElementById('preview-gallery');
if (previews && previews.length > 0) {
gallery.innerHTML = "";
previews.forEach(p => {
const dateStr = new Date(p.mtime * 1000).toLocaleTimeString();
gallery.innerHTML += `
<div class="preview-img-container">
<img src="/api/preview/file/${p.path}?t=${p.mtime}" class="preview-img" alt="${p.name}">
<div class="preview-meta">${p.name} - ${dateStr}</div>
</div>
`;
});
} else {
gallery.innerHTML = `<div class="no-preview-text">No previews found. Start training to generate real-time previews.</div>`;
}
})
.catch(err => {
console.error("Failed to fetch previews:", err);
});
}
function uploadFile(input, target) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('target', target);
const progressContainer = document.getElementById(`progress-${target}`);
const progressBar = document.getElementById(`progress-bar-${target}`);
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressBar.style.width = percent + '%';
}
};
xhr.onload = function() {
if (xhr.status === 200) {
alert(`Successfully uploaded ${file.name} to workspace!`);
updateSystemStatus();
} else {
const res = JSON.parse(xhr.responseText);
alert(`Upload failed: ${res.error}`);
}
progressContainer.style.display = 'none';
};
xhr.onerror = function() {
alert('Upload failed: network error');
progressContainer.style.display = 'none';
};
xhr.send(formData);
}
</script>
</body>
</html>