anycoder-001cbd22 / index.html
bleckhert's picture
Upload folder using huggingface_hub
3744f6d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FaceSwap Studio - AI Video Processor</title>
<!-- Importing FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-dark: #0f0f13;
--bg-panel: #16161a;
--bg-input: #1e1e24;
--primary: #6366f1;
--primary-hover: #4f46e5;
--accent: #ec4899;
--text-main: #ffffff;
--text-muted: #a1a1aa;
--border: #2d2d3a;
--glass: rgba(22, 22, 26, 0.8);
--gradient: linear-gradient(135deg, #6366f1 0%, #ec4899 100%);
--shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
--glow: 0 0 20px rgba(99, 66, 241, 0.3);
}
* {
box-sizing: box-sizing;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
height: 100vh;
overflow-x: hidden;
display: flex;
flex-direction: column;
font-size: 14px;
}
/* --- Header --- */
header {
height: 64px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2rem;
background: var(--bg-panel);
z-index: 100;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.logo {
font-weight: 700;
font-size: 1.2rem;
letter-spacing: -0.5px;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 12px;
}
.anycoder-link {
font-size: 0.8rem;
color: var(--text-muted);
text-decoration: none;
border: 1px solid var(--border);
padding: 6px 14px;
border-radius: 6px;
transition: all 0.3s ease;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
background: rgba(15, 15, 19, 0.5);
}
.anycoder-link:hover {
border-color: var(--primary);
color: var(--primary);
background: rgba(99, 66, 241, 0.1);
}
/* --- Main Layout --- */
main {
display: grid;
grid-template-columns: 340px 1fr;
height: calc(100vh - 64px);
overflow: hidden;
}
/* --- Sidebar (Inputs) --- */
.sidebar {
background: var(--bg-panel);
border-right: 1px solid var(--border);
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
overflow-y: auto;
}
.input-card {
background: var(--bg-input);
border: 1px dashed var(--border);
border-radius: 12px;
padding: 20px;
text-align: center;
transition: 0.3s;
cursor: pointer;
position: relative;
min-height: 160px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
.input-card:hover {
border-color: var(--primary);
background: rgba(99, 66, 241, 0.05);
}
.input-card.active {
border-color: var(--accent);
border-style: solid;
box-shadow: 0 0 15px rgba(236, 72, 153, 0.15);
}
.input-icon {
font-size: 2.5rem;
color: var(--text-muted);
margin-bottom: 12px;
transition: 0.3s;
}
.input-card:hover .input-icon {
color: var(--primary);
transform: scale(1.1);
}
.input-text {
font-size: 0.9rem;
color: var(--text-muted);
}
.preview-img {
width: 100%;
height: 100px;
border-radius: 8px;
display: none;
margin-top: 10px;
object-fit: cover;
border: 1px solid var(--border);
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.file-info {
margin-top: 8px;
font-size: 0.75rem;
color: var(--primary);
font-weight: 600;
}
.settings-group {
margin-top: 20px;
background: rgba(0,0,0,0.2);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border);
}
.setting-item {
margin-bottom: 10px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
}
.toggle-switch {
width: 32px;
height: 16px;
background: var(--border);
border-radius: 8px;
position: relative;
cursor: pointer;
transition: 0.3s;
}
.toggle-switch.active {
background: var(--primary);
}
.toggle-switch::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 10px;
height: 10px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.toggle-switch.active::after {
left: 18px;
}
/* --- Workspace (Video & Timeline) --- */
.workspace {
display: flex;
flex-direction: column;
padding: 24px;
gap: 20px;
position: relative;
background: radial-gradient(circle at center, #1a1a24 0%, #0f0f13 100%);
}
/* Video Container */
.video-stage {
flex-grow: 1;
background: #000;
border-radius: 16px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
box-shadow: var(--shadow);
}
video,
canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 4px;
}
/* Overlay Controls */
.video-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), transparent);
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
}
.status-badge {
background: rgba(15, 15, 19, 0.8);
padding: 6px 12px;
border-radius: 4px;
font-size: 0.75rem;
border: 1px solid var(--border);
display: flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(4px);
}
.status-badge.processing {
border-color: var(--accent);
color: var(--accent);
animation: pulse 1.5s infinite;
}
.time-display {
font-family: monospace;
color: var(--text-muted);
font-size: 0.9rem;
}
/* Timeline Section */
.timeline-panel {
height: 240px;
background: var(--bg-panel);
border-radius: 12px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.timeline-header {
padding: 12px 20px;
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
background: rgba(15, 15, 19, 0.3);
align-items: center;
}
.timeline-scroll-area {
flex-grow: 1;
display: flex;
overflow-x: auto;
padding: 15px;
gap: 8px;
scrollbar-width: thin;
scrollbar-color: var(--primary) var(--bg-dark);
align-items: center;
background: #0f0f13;
}
/* Custom Scrollbar */
.timeline-scroll-area::-webkit-scrollbar {
height: 8px;
}
.timeline-scroll-area::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.frame-thumb {
min-width: 70px;
height: 90px;
background: #000;
border-radius: 6px;
flex-shrink: 0;
position: relative;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.frame-thumb:hover {
transform: translateY(-4px);
z-index: 2;
border-color: var(--text-muted);
}
.frame-thumb.active {
border-color: var(--primary);
transform: translateY(-4px) scale(1.05);
box-shadow: 0 0 15px rgba(99, 66, 241, 0.4);
z-index: 3;
}
.frame-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.frame-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(99, 66, 241, 0.3);
display: none;
}
.frame-thumb.processed .frame-overlay {
display: block;
}
.frame-number {
position: absolute;
bottom: 4px;
right: 4px;
font-size: 0.6rem;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 1px 4px;
border-radius: 2px;
pointer-events: none;
}
/* Action Bar */
.action-bar {
margin-top: 15px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn {
padding: 10px 24px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
border: none;
transition: 0.2s;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background: var(--gradient);
color: white;
box-shadow: 0 4px 15px rgba(99, 66, 241, 0.4);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(99, 66, 241, 0.6);
}
.btn-secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text-muted);
}
.btn-secondary:hover {
border-color: var(--text-main);
color: var(--text-main);
}
/* --- Animations --- */
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
/* --- Responsive --- */
@media (max-width: 900px) {
main {
grid-template-columns: 1fr;
height: auto;
overflow: auto;
}
.sidebar {
border-right: none;
border-bottom: 1px solid var(--border);
padding: 20px;
}
.workspace {
height: auto;
}
.timeline-panel {
height: 200px;
}
}
/* Hidden inputs */
#source-input, #target-input { display: none; }
/* Progress Bar Overlay */
.progress-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(15, 15, 19, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 200;
backdrop-filter: blur(8px);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.progress-overlay.visible {
opacity: 1;
pointer-events: all;
}
.progress-track {
width: 300px;
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
margin-top: 15px;
}
.progress-fill {
height: 100%;
background: var(--gradient);
width: 0%;
transition: width 0.2s linear;
}
.loader-text {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 10px;
background: var(--gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<header>
<div class="logo">
<i class="fa-solid fa-layer-group"></i> FaceSwap Studio
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder <i class="fa-solid fa-external-link-alt"></i>
</a>
</header>
<main>
<!-- Sidebar: Inputs -->
<div class="sidebar">
<!-- Source Image Input -->
<div class="input-card" id="source-zone" onclick="document.getElementById('source-input').click()">
<input type="file" id="source-input" accept="image/*">
<i class="fa-solid fa-user input-icon"></i>
<div class="input-text">
<strong>Source Face</strong><br>
Upload reference image
</div>
<img id="source-preview" class="preview-img" alt="Source Face">
<div id="source-info" class="file-info"></div>
</div>
<!-- Target Video Input -->
<div class="input-card" id="target-zone" onclick="document.getElementById('target-input').click()">
<input type="file" id="target-input" accept="video/*">
<i class="fa-solid fa-video input-icon"></i>
<div class="input-text">
<strong>Target Video</strong><br>
Upload video to process
</div>
<div id="target-info" class="file-info"></div>
</div>
<div class="settings-group">
<div class="setting-item">
<span>Enhance Lighting</span>
<div class="toggle-switch active"></div>
</div>
<div class="setting-item">
<span>Smooth Blending</span>
<div class="toggle-switch active"></div>
</div>
<div class="setting-item">
<span>Face Tracking</span>
<div class="toggle-switch active"></div>
</div>
</div>
<div style="margin-top: 20px; font-size: 0.85rem; color: var(--text-muted); line-height: 1.6; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px;">
<p><i class="fa-solid fa-circle-info"></i> <strong>Workflow:</strong></p>
<p>1. Upload a clear face image.</p>
<p>2. Upload a target video.</p>
<p>3. Click "Start Swap" to process.</p>
</div>
</div>
<!-- Workspace -->
<div class="workspace">
<!-- Video Player / Canvas Output -->
<div class="video-stage">
<video id="main-video" muted loop playsinline></video>
<!-- Canvas for processing preview -->
<canvas id="process-canvas" style="display:none;"></canvas>
<div class="video-overlay">
<div class="status-badge" id="processing-status">
<i class="fa-solid fa-circle-notch"></i> Ready
</div>
<div class="time-display">
<span id="current-time">00:00</span> / <span id="total-time">00:00</span>
</div>
</div>
</div>
<!-- Timeline / Frame Visualizer -->
<div class="timeline-panel">
<div class="timeline-header">
<span><i class="fa-solid fa-bars"></i> Frame Timeline</span>
<span id="frame-count">0 Frames</span>
</div>
<div class="timeline-scroll-area" id="timeline-track">
<!-- Frames will be injected here via JS -->
<div style="display:flex; align-items:center; justify-content:center; width:100%; color: var(--text-muted); font-style: italic;">
Upload a video to generate timeline
</div>
</div>
<div class="action-bar">
<button class="btn btn-secondary" onclick="resetApp()">
<i class="fa-solid fa-rotate-left"></i> Reset
</button>
<button class="btn btn-primary" onclick="startSwapProcess()">
<i class="fa-solid fa-magic"></i> Start Face Swap
</button>
</div>
</div>
</div>
</main>
<!-- Processing Overlay -->
<div class="progress-overlay" id="progress-overlay">
<div class="loader-text">Processing Frames...</div>
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div style="margin-top: 10px; color: var(--text-muted); font-size: 0.9rem;">
Applying AI transformations...
</div>
</div>
<script>
// --- DOM Elements ---
const sourceInput = document.getElementById('source-input');
const targetInput = document.getElementById('target-input');
const sourcePreview = document.getElementById('source-preview');
const sourceZone = document.getElementById('source-zone');
const sourceInfo = document.getElementById('source-info');
const targetInfo = document.getElementById('target-info');
const mainVideo = document.getElementById('main-video');
const processCanvas = document.getElementById('process-canvas');
const timelineTrack = document.getElementById('timeline-track');
const frameCountLabel = document.getElementById('frame-count');
const processingStatus = document.getElementById('processing-status');
const currentTimeLabel = document.getElementById('current-time');
const totalTimeLabel = document.getElementById('total-time');
const progressOverlay = document.getElementById('progress-overlay');
const progressFill = document.getElementById('progress-fill');
let sourceImageObj = new Image();
let isProcessing = false;
let extractedFrames = [];
let videoDuration = 0;
// --- Event Listeners ---
// Source Image Upload
sourceInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
sourceImageObj.src = event.target.result;
sourcePreview.src = event.target.result;
sourcePreview.style.display = 'block';
sourceZone.classList.add('active');
sourceInfo.innerText = file.name;
};
reader.readAsDataURL(file);
}
});
// Target Video Upload
targetInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const url = URL.createObjectURL(file);
mainVideo.src = url;
targetInfo.innerText = file.name;
mainVideo.onloadeddata = () => {
videoDuration = mainVideo.duration;
totalTimeLabel.innerText = formatTime(videoDuration);
generateTimelineThumbnails();
};
}
});
// Video Time Update
mainVideo.addEventListener('timeupdate', () => {
currentTimeLabel.innerText = formatTime(mainVideo.currentTime);
highlightCurrentFrame();
});
// --- Functions ---
function formatTime(seconds) {
if(isNaN(seconds)) return "00:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
}
// Generate Timeline Thumbnails
function generateTimelineThumbnails() {
timelineTrack.innerHTML = '';
extractedFrames = [];
const duration = mainVideo.duration;
const interval = 0.5; // Generate thumb every 0.5s
const totalFrames = Math.floor(duration / interval);
frameCountLabel.innerText = `${totalFrames} Frames`;
// We use a temporary canvas to grab frames from the video
const tempCanvas = document.createElement('canvas');
tempCanvas.width = 70;
tempCanvas.height = 90;
const ctx = tempCanvas.getContext('2d');
for (let i = 0; i < totalFrames; i++) {
const thumb = document.createElement('div');
thumb.className = 'frame-thumb';
thumb.dataset.index = i;
thumb.dataset.time = i * interval;
// Capture frame logic
mainVideo.currentTime = i * interval;
mainVideo.onseeked = () => {
ctx.drawImage(mainVideo, 0, 0, 70, 90, 0, 0, 70, 90);
thumb.style.backgroundImage = `url(${tempCanvas.toDataURL()})`;
thumb.style.backgroundSize = 'cover';
};
const frameNum = document.createElement('div');
frameNum.className = 'frame-number';
frameNum.innerText = i;
thumb.appendChild(frameNum);
thumb.addEventListener('click', () => {
mainVideo.currentTime = i * interval;
mainVideo.play();
});
timelineTrack.appendChild(thumb);
extractedFrames.push(thumb);
}
}
function highlightCurrentFrame() {
const currentSec = mainVideo.currentTime;
const thumbIndex = Math.floor(currentSec / 0.5);
extractedFrames.forEach(t => t.classList.remove('active'));
if(extractedFrames[thumbIndex]) {
extractedFrames[thumbIndex].classList.add('active');
}
}
// --- The "Swap" Logic (Simulation) ---
// Since we cannot run heavy Python/DeepLearning in a single HTML file,
// we simulate the UI experience of processing frames and perform a basic overlay.
function startSwapProcess() {
if (!sourceImageObj.src || !mainVideo.src) {
alert("Please upload both a Source Face and a Target Video.");
return;
}
if (isProcessing) return;
isProcessing = true;
// Show Progress Overlay
progressOverlay.classList.add('visible');
processingStatus.classList.add('processing');
processingStatus.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Processing...`;
mainVideo.pause();
// Hide video, show canvas for "processing"
mainVideo.style.display = 'none';
processCanvas.style.display = 'block';
// Setup canvas dimensions (using fixed size for demo stability)
processCanvas.width = 640;
processCanvas.height = 360;
const ctx = processCanvas.getContext('2d');
let frameIndex = 0;
const totalFrames = extractedFrames.length || 100; // Default fallback if empty
const speed = 50; // ms per frame (fast for demo)
// Animation Loop
const interval = setInterval(() => {
// 1. Clear
ctx.clearRect(0, 0, processCanvas.width, processCanvas.height);
// 2. Draw "Video Frame" (Simulated background)
ctx.fillStyle = '#1a1a24';
ctx.fillRect(0, 0, processCanvas.width, processCanvas.height);
// 3. Draw "Swapped Face" (The Source Image)
if(sourceImageObj) {
const imgSize = 150;
const x = (processCanvas.width / 2) - (imgSize / 2);
const y = (processCanvas.height / 2) - (imgSize / 2);
// Simulate "AI Tracking" by slightly moving the face based on sine wave
const offset = Math.sin(frameIndex * 0.1) * 10;
ctx.drawImage(sourceImageObj, x + offset, y + offset, imgSize, imgSize);
// Add a "Scanline" effect to look like processing
ctx.fillStyle = `rgba(99, 66, 241, 0.2)`;
ctx.fillRect(0, frameIndex % processCanvas.height, processCanvas.width, 2);
}
// Update Timeline UI
if(extractedFrames[frameIndex]) {
extractedFrames[frameIndex].classList.add('processed');
extractedFrames[frameIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
frameIndex++;
// Update UI progress
const progress = Math.min((frameIndex / totalFrames) * 100, 100);
progressFill.style.width = `${progress}%`;
processingStatus.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Processing ${Math.floor(progress)}%`;
if (frameIndex >= totalFrames) {
clearInterval(interval);
finishProcessing();
}
}, speed);
}
function finishProcessing() {
isProcessing = false;
processingStatus.classList.remove('processing');
processingStatus.innerHTML = `<i class="fa-solid fa-check"></i> Completed`;
processingStatus.style.borderColor = "#4ade80";
processingStatus.style.color = "#4ade80";
// Hide overlay
progressOverlay.classList.remove('visible');
// Reset view
setTimeout(() => {
processCanvas.style.display = 'none';
mainVideo.style.display = 'block';
mainVideo.play();
processingStatus.style.borderColor = "var(--border)";
processingStatus.style.color = "var(--text-main)";
processingStatus.innerHTML = `<i class="fa-solid fa-circle"></i> Ready`;
// Reset progress bar
progressFill.style.width = '0%';
}, 1500);
}
function resetApp() {
location.reload();
}
</script>
</body>
</html>