1118 lines
39 KiB
HTML
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>
|