anycoder-92f045e9 / index.html
yat343's picture
Upload folder using huggingface_hub
f245885 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Video Converter - FFmpeg.wasm</title>
<!-- FontAwesome for Icons -->
<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&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--primary: #6366f1;
--primary-hover: #4f46e5;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bg-input: #334155;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--border: #475569;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--radius: 12px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
outline: none;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
/* Header */
header {
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.25rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-main);
text-decoration: none;
}
.logo i { color: var(--primary); }
/* Main Layout */
main {
flex: 1;
display: grid;
grid-template-columns: 350px 1fr;
gap: 2rem;
padding: 2rem;
max-width: 1600px;
margin: 0 auto;
width: 100%;
}
@media (max-width: 900px) {
main {
grid-template-columns: 1fr;
}
}
/* Panels */
.panel {
background: var(--bg-card);
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: var(--shadow);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
height: fit-content;
}
h2 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-main);
display: flex;
align-items: center;
gap: 0.5rem;
}
/* File Upload Area */
.upload-area {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
background: rgba(51, 65, 85, 0.3);
}
.upload-area:hover, .upload-area.dragover {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
.upload-icon {
font-size: 3rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.upload-text {
color: var(--text-muted);
font-size: 0.9rem;
}
.upload-text strong { color: var(--primary); }
#fileInput {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* File Info */
.file-info {
display: none;
background: var(--bg-input);
padding: 1rem;
border-radius: var(--radius);
animation: fadeIn 0.3s ease;
}
.file-info.active { display: block; }
.file-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.file-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 70%; }
.file-size { color: var(--text-muted); }
/* Controls */
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.control-header {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-muted);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: var(--bg-input);
border-radius: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: var(--primary);
border-radius: 50%;
cursor: pointer;
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
.resolution-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-top: 0.5rem;
}
.res-btn {
background: var(--bg-input);
border: 1px solid transparent;
color: var(--text-muted);
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.res-btn:hover { background: var(--border); }
.res-btn.active { background: rgba(99, 102, 241, 0.2); color: var(--primary); border-color: var(--primary); }
select {
width: 100%;
padding: 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-main);
font-family: inherit;
cursor: pointer;
}
/* Action Button */
.btn-primary {
width: 100%;
padding: 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
transition: background 0.3s;
}
.btn-primary:hover { background: var(--primary-hover); }
.btn-primary:disabled { background: var(--bg-input); cursor: not-allowed; opacity: 0.7; }
/* Terminal / Logs */
.terminal-container {
background: #000;
border-radius: var(--radius);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
height: 100%;
min-height: 400px;
overflow: hidden;
}
.terminal-header {
background: var(--bg-input);
padding: 0.75rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
}
.terminal-title { font-size: 0.85rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }
.terminal-actions button {
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 0.25rem;
}
.terminal-actions button:hover { color: var(--text-main); }
#terminal {
flex: 1;
padding: 1rem;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
overflow-y: auto;
color: #d4d4d4;
line-height: 1.5;
}
.log-line { margin-bottom: 0.25rem; word-wrap: break-word; }
.log-info { color: #60a5fa; }
.log-success { color: var(--success); }
.log-error { color: var(--error); }
.log-process { color: #fbbf24; }
.log-progress { color: var(--text-muted); }
/* Progress Bar */
.progress-container {
margin-top: 1rem;
}
.progress-labels {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--text-muted);
}
.progress-track {
height: 8px;
background: var(--bg-input);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--primary), #818cf8);
transition: width 0.3s ease;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-size: 0.85rem;
margin-top: auto;
border-top: 1px solid var(--border);
}
footer a {
color: var(--primary);
text-decoration: none;
font-weight: 600;
}
/* Utilities */
.hidden { display: none !important; }
.badge {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
background: var(--bg-input);
color: var(--text-muted);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-dark); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
</style>
</head>
<body>
<header>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="logo">
<i class="fa-solid fa-layer-group"></i>
Anycoder
</a>
<div style="display: flex; gap: 1rem; align-items: center;">
<span class="badge" style="background: rgba(16, 185, 129, 0.1); color: var(--success);">WebAssembly Ready</span>
</div>
</header>
<main>
<!-- Left Panel: Controls -->
<section class="panel">
<h2><i class="fa-solid fa-sliders"></i> Settings</h2>
<!-- File Input -->
<div class="upload-area" id="dropZone">
<i class="fa-solid fa-cloud-arrow-up upload-icon"></i>
<div class="upload-text">
<strong>Click to upload</strong> or drag and drop<br>
MP4, MOV, AVI, MKV (Max 10GB+)
</div>
<input type="file" id="fileInput" accept="video/*">
</div>
<div class="file-info" id="fileInfo">
<div class="file-row">
<span class="file-name" id="fileName">video.mp4</span>
<span class="file-size" id="fileSize">0 GB</span>
</div>
<div class="file-row">
<span class="badge" id="fileDuration">Duration: --:--</span>
<span class="badge" id="fileRes">Original: --x--</span>
</div>
</div>
<!-- Resolution Controls -->
<div class="control-group">
<div class="control-header">
<span>Resolution</span>
<span id="resValue">100%</span>
</div>
<input type="range" id="resolutionSlider" min="10" max="100" value="100">
<div class="resolution-grid">
<button class="res-btn" data-val="0.5">0.5x</button>
<button class="res-btn active" data-val="1.0">1.0x</button>
<button class="res-btn" data-val="0.25">0.25x</button>
</div>
</div>
<!-- Bitrate Controls -->
<div class="control-group">
<div class="control-header">
<span>Target Bitrate</span>
<span id="bitrateValue">2.0 Mbps</span>
</div>
<input type="range" id="bitrateSlider" min="500" max="10000" step="100" value="2000">
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;">
Lower values = Smaller file size, potentially lower quality.
</div>
</div>
<!-- Codec Selection -->
<div class="control-group">
<label style="font-size: 0.9rem; color: var(--text-muted);">Video Codec</label>
<select id="codecSelect">
<option value="libx264">H.264 (Best Compatibility)</option>
<option value="libx265">H.265 (HEVC - Smaller, CPU Heavy)</option>
<option value="libvpx-vp9">VP9 (Google, Good Compression)</option>
<option value="copy">Copy Stream (No Re-encoding)</option>
</select>
</div>
<!-- Action Button -->
<button id="convertBtn" class="btn-primary" disabled>
<i class="fa-solid fa-play"></i> Start Conversion
</button>
</section>
<!-- Right Panel: Terminal & Progress -->
<section class="panel" style="padding: 0;">
<div class="terminal-container">
<div class="terminal-header">
<span class="terminal-title">FFmpeg Terminal</span>
<div class="terminal-actions">
<button id="clearLogs" title="Clear Logs"><i class="fa-solid fa-trash"></i></button>
<button id="downloadBtn" title="Download Result" class="hidden"><i class="fa-solid fa-download"></i></button>
</div>
</div>
<div id="terminal">
<div class="log-line log-info">[System] Ready. Waiting for input...</div>
<div class="log-line log-info">[System] FFmpeg.wasm loaded.</div>
</div>
</div>
<div class="progress-container">
<div class="progress-labels">
<span id="progressPercent">0%</span>
<span id="progressTime">ETA: --:--</span>
</div>
<div class="progress-track">
<div class="progress-fill" id="progressBar"></div>
</div>
<div id="statusText" style="text-align: center; font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem;">
Idle
</div>
</div>
</section>
</main>
<footer>
<p>Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a></p>
</footer>
<!-- Import Map for FFmpeg.wasm -->
<script type="importmap">
{
"imports": {
"ffmpeg": "https://unpkg.com/@ffmpeg/ffmpeg@0.12.7/dist/esm/index.js",
"@ffmpeg/util": "https://unpkg.com/@ffmpeg/util@0.12.1/dist/esm/index.js"
}
}
</script>
<script type="module">
import { FFmpeg } from 'ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
// --- DOM Elements ---
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const fileInfo = document.getElementById('fileInfo');
const fileNameEl = document.getElementById('fileName');
const fileSizeEl = document.getElementById('fileSize');
const fileDurationEl = document.getElementById('fileDuration');
const fileResEl = document.getElementById('fileRes');
const resolutionSlider = document.getElementById('resolutionSlider');
const resValue = document.getElementById('resValue');
const resBtns = document.querySelectorAll('.res-btn');
const bitrateSlider = document.getElementById('bitrateSlider');
const bitrateValue = document.getElementById('bitrateValue');
const codecSelect = document.getElementById('codecSelect');
const convertBtn = document.getElementById('convertBtn');
const terminal = document.getElementById('terminal');
const progressBar = document.getElementById('progressBar');
const progressPercent = document.getElementById('progressPercent');
const progressTime = document.getElementById('progressTime');
const statusText = document.getElementById('statusText');
const downloadBtn = document.getElementById('downloadBtn');
const clearLogsBtn = document.getElementById('clearLogs');
// --- State ---
let ffmpeg = null;
let loaded = false;
let selectedFile = null;
let originalWidth = 0;
let originalHeight = 0;
let isConverting = false;
// --- Initialization ---
async function loadFFmpeg() {
if (loaded) return;
log('info', 'Loading FFmpeg Core...');
ffmpeg = new FFmpeg();
// Event Listeners for FFmpeg progress
ffmpeg.on('log', ({ message }) => {
log('process', message);
});
ffmpeg.on('progress', ({ progress, time }) => {
if (time > 0) {
const percent = Math.round(progress * 100);
progressBar.style.width = `${percent}%`;
progressPercent.innerText = `${percent}%`;
// Simple ETA calculation
if (progress > 0 && progress < 1) {
const totalSeconds = (time / progress);
const remainingSeconds = totalSeconds - time;
progressTime.innerText = `ETA: ${formatTime(remainingSeconds)}`;
}
}
});
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
try {
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
loaded = true;
log('success', 'FFmpeg Core Loaded Successfully!');
log('info', 'Ready to process large files.');
} catch (error) {
log('error', 'Failed to load FFmpeg: ' + error.message);
console.error(error);
}
}
// --- File Handling ---
function formatBytes(bytes, decimals = 2) {
if (!+bytes) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
function formatTime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
return `${m}:${s.toString().padStart(2, '0')}`;
}
function handleFile(file) {
if (!file.type.startsWith('video/')) {
log('error', 'Please select a valid video file.');
return;
}
if (file.size > 10 * 1024 * 1024 * 1024) {
log('error', 'File is too large (Max 10GB supported).');
return;
}
selectedFile = file;
fileNameEl.textContent = file.name;
fileSizeEl.textContent = formatBytes(file.size);
fileInfo.classList.add('active');
dropZone.classList.add('hidden');
convertBtn.disabled = false;
// Basic metadata extraction (using a small helper or just assuming standard)
// Since we can't easily get duration/width/height without re-encoding first or using ffprobe
// We will set defaults and update if possible.
fileResEl.textContent = "Detecting...";
// We use a simple timeout to simulate initial load
setTimeout(() => {
fileResEl.textContent = "Original: --x-- (Detecting via FFprobe)";
// Note: Real width/height detection requires ffprobe,
// but we can proceed with the current settings.
}, 500);
}
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) handleFile(e.target.files[0]);
});
// Drag and Drop
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]);
});
// --- Controls Logic ---
resolutionSlider.addEventListener('input', (e) => {
resValue.textContent = `${e.target.value}%`;
resBtns.forEach(btn => btn.classList.remove('active'));
});
resBtns.forEach(btn => {
btn.addEventListener('click', () => {
resBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const scale = parseFloat(btn.dataset.val);
resolutionSlider.value = scale * 100;
resValue.textContent = `${scale}x`;
});
});
bitrateSlider.addEventListener('input', (e) => {
bitrateValue.textContent = `${(e.target.value / 1000).toFixed(1)} Mbps`;
});
// --- Conversion Logic ---
convertBtn.addEventListener('click', async () => {
if (!selectedFile || !loaded) return;
isConverting = true;
convertBtn.disabled = true;
convertBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Converting...';
statusText.textContent = "Initializing FFmpeg...";
downloadBtn.classList.add('hidden');
progressBar.style.width = '0%';
// Create output filename
const nameParts = selectedFile.name.split('.');
const ext = nameParts.pop();
const baseName = nameParts.join('.');
const outputName = `${baseName}_converted.mp4`;
try {
// 1. Load File into FFmpeg memory
log('info', `Loading file into memory (${formatBytes(selectedFile.size)})...`);
statusText.textContent = "Loading File...";
await ffmpeg.writeFile('input.mp4', await fetchFile(selectedFile));
// 2. Determine Output Dimensions
const scale = parseFloat(resolutionSlider.value) / 100;
const newWidth = Math.round(originalWidth * scale);
const newHeight = Math.round(originalHeight * scale);
// If scale is 1.0 (100%), FFmpeg handles it automatically with -vf scale=iw:-2
let scaleFilter = '';
if (scale < 1) {
scaleFilter = `-vf scale=${newWidth}:-2`; // -2 ensures height is even
}
// 3. Determine Codec
const codec = codecSelect.value;
let videoCodecArgs = '';
if (codec === 'libx264') videoCodecArgs = '-preset veryfast -crf 23';
if (codec === 'libx265') videoCodecArgs = '-preset veryfast -crf 28'; // H265 needs higher CRF for same quality
if (codec === 'libvpx-vp9') videoCodecArgs = '-b:v 1M -quality good -speed 4';
if (codec === 'copy') videoCodecArgs = '-c:v copy'; // No re-encoding
// 4. Construct FFmpeg Command
// -movflags +faststart improves web playback performance
const bitrate = parseInt(bitrateSlider.value);
const args = [
'-i', 'input.mp4',
'-c:a', 'aac', // Force AAC audio for compatibility
'-b:a', '128k',
'-b:v', `${bitrate}k`,
'-movflags', '+faststart',
'-y', // Overwrite output
outputName
];
if (videoCodecArgs) args.unshift(...videoCodecArgs.split(' '));
if (scaleFilter) args.unshift(...scaleFilter.split(' '));
log('info', `Starting conversion: ${args.join(' ')}`);
// 5. Run FFmpeg
statusText.textContent = "Processing Video...";
await ffmpeg.exec(args);
// 6. Read Result
statusText.textContent = "Writing output file...";
const data = await ffmpeg.readFile(outputName);
// 7. Download
const blob = new Blob([data.buffer], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
downloadBtn.href = url;
downloadBtn.download = outputName;
downloadBtn.classList.remove('hidden');
log('success', `Conversion Complete! Output size: ${formatBytes(blob.size)}`);
log('success', 'Click the download button to save.');
statusText.textContent = "Conversion Complete";
convertBtn.innerHTML = '<i class="fa-solid fa-check"></i> Done';
convertBtn.disabled = false;
} catch (error) {
log('error', `Error: ${error.message}`);
statusText.textContent = "Conversion Failed";
convertBtn.innerHTML = '<i class="fa-solid fa-play"></i> Retry';
convertBtn.disabled = false;
} finally {
isConverting = false;
}
});
// --- Terminal & UI Helpers ---
function log(type, message) {
const div = document.createElement('div');
div.className = `log-line log-${type}`;
// Timestamp
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
div.innerHTML = `<span style="opacity:0.5">[${time}]</span> ${message}`;
terminal.appendChild(div);
terminal.scrollTop = terminal.scrollHeight;
}
clearLogsBtn.addEventListener('click', () => {
terminal.innerHTML = '<div class="log-line log-info">[System] Logs cleared.</div>';
});
downloadBtn.addEventListener('click', () => {
// Small delay to ensure browser recognizes the new blob
setTimeout(() => {
log('info', 'Download started.');
}, 100);
});
// Initialize on Load
window.addEventListener('load', loadFFmpeg);
</script>
</body>
</html>