anycoder-df712dae / index.html
efawggthsa's picture
Upload folder using huggingface_hub
eda5bbe verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CLIP Batch Image Classifier</title>
<style>
:root {
--bg-color: #0f172a;
--surface-color: #1e293b;
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--accent-color: #10b981;
--text-main: #f8fafc;
--text-secondary: #94a3b8;
--border-color: #334155;
--radius-md: 12px;
--radius-lg: 16px;
--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;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
header {
background-color: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 1.25rem;
color: var(--text-main);
}
.brand-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary-color), var(--accent-color));
border-radius: 8px;
display: grid;
place-items: center;
font-size: 1.2rem;
}
.anycoder-link {
font-size: 0.875rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s;
border: 1px solid var(--border-color);
padding: 6px 12px;
border-radius: 20px;
}
.anycoder-link:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
/* 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;
}
}
/* Sidebar / Controls */
.sidebar {
background-color: var(--surface-color);
padding: 1.5rem;
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
height: fit-content;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
h2 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--text-main);
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
textarea {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-main);
padding: 0.75rem;
font-size: 0.95rem;
resize: vertical;
min-height: 80px;
outline: none;
transition: border-color 0.2s;
}
textarea:focus {
border-color: var(--primary-color);
}
/* File Input Styling */
.file-drop-zone {
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
background-color: rgba(255, 255, 255, 0.02);
}
.file-drop-zone:hover,
.file-drop-zone.dragover {
border-color: var(--primary-color);
background-color: rgba(59, 130, 246, 0.05);
}
.file-drop-zone input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.file-info {
font-size: 0.875rem;
color: var(--text-secondary);
pointer-events: none;
}
.file-count {
color: var(--primary-color);
font-weight: bold;
display: block;
margin-top: 5px;
}
/* Buttons */
.btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.875rem;
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn:hover {
background-color: var(--primary-hover);
}
.btn:active {
transform: scale(0.98);
}
.btn:disabled {
background-color: var(--border-color);
cursor: not-allowed;
opacity: 0.7;
}
/* Model Status */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
background-color: rgba(255, 255, 255, 0.1);
}
.status-badge.loading {
color: #fbbf24;
background-color: rgba(251, 191, 36, 0.1);
}
.status-badge.ready {
color: var(--accent-color);
background-color: rgba(16, 185, 129, 0.1);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: currentColor;
}
.status-badge.loading .status-dot {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
opacity: 0.4;
}
50% {
opacity: 1;
}
100% {
opacity: 0.4;
}
}
/* Results Area */
.results-area {
display: flex;
flex-direction: column;
gap: 1rem;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
text-align: center;
padding: 4rem;
background-color: var(--surface-color);
border-radius: var(--radius-lg);
border: 1px dashed var(--border-color);
}
/* Grid */
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
/* Image Card */
.image-card {
background-color: var(--surface-color);
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
}
.image-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
border-color: var(--primary-color);
}
.card-img-wrapper {
width: 100%;
height: 200px;
background-color: #000;
position: relative;
overflow: hidden;
}
.card-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.image-card:hover .card-img {
transform: scale(1.05);
}
.card-content {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.score-row {
margin-top: 0.5rem;
}
.score-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 4px;
}
.score-bar-bg {
height: 6px;
background-color: var(--bg-color);
border-radius: 3px;
overflow: hidden;
}
.score-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
width: 0%;
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Toast */
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
background-color: var(--surface-color);
border-left: 4px solid var(--primary-color);
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
color: var(--text-main);
font-size: 0.9rem;
animation: slideIn 0.3s ease-out;
max-width: 300px;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Loader */
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="brand-icon">🔮</div>
<span>NeuroSort CLIP</span>
</div>
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">
Built with anycoder
</a>
</header>
<main>
<!-- Sidebar Controls -->
<aside class="sidebar">
<!-- Model Status -->
<div class="control-group">
<label>System Status</label>
<div id="modelStatus" class="status-badge loading">
<div class="status-dot"></div>
<span id="statusText">Initializing Model...</span>
</div>
</div>
<hr style="border: 0; border-top: 1px solid var(--border-color);">
<!-- Text Input -->
<div class="control-group">
<label for="textInput">Natural Language Query</label>
<textarea id="textInput" placeholder="Describe what you want to find (e.g., 'a futuristic city at night', 'a golden retriever', 'a red sports car')..."></textarea>
</div>
<!-- Image Upload -->
<div class="control-group">
<label>Batch Images</label>
<div class="file-drop-zone" id="dropZone">
<input type="file" id="fileInput" multiple accept="image/png, image/jpeg, image/webp, image/gif">
<div class="file-info">
<span>Click or Drag & Drop images here</span>
<span class="file-count" id="fileCount">0 files selected</span>
</div>
</div>
</div>
<!-- Action Button -->
<button id="classifyBtn" class="btn" disabled>
<span>Classify Images</span>
</button>
</aside>
<!-- Results Section -->
<section class="results-area">
<div class="results-header">
<h2>Classification Results</h2>
<span style="font-size: 0.875rem; color: var(--text-secondary);" id="resultCount"></span>
</div>
<div id="resultsContainer">
<div class="empty-state">
<div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;">🖼️</div>
<h3>No results yet</h3>
<p>Upload images and enter a prompt to start classifying.</p>
</div>
</div>
</section>
</main>
<div class="toast-container" id="toastContainer"></div>
<!-- Import Transformers.js -->
<script type="module">
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.14.0';
// Configuration
env.allowLocalModels = false;
env.useBrowserCache = true;
// DOM Elements
const modelStatus = document.getElementById('modelStatus');
const statusText = document.getElementById('statusText');
const textInput = document.getElementById('textInput');
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const fileCount = document.getElementById('fileCount');
const classifyBtn = document.getElementById('classifyBtn');
const resultsContainer = document.getElementById('resultsContainer');
const resultCount = document.getElementById('resultCount');
const toastContainer = document.getElementById('toastContainer');
// State
let classifier = null;
let selectedFiles = [];
// --- 1. Model Loading ---
async function loadModel() {
try {
statusText.textContent = "Downloading CLIP Model (60MB)...";
// Using Xenova's CLIP model optimized for browser
classifier = await pipeline('zero-shot-image-classification', 'Xenova/clip-vit-base-patch32', {
progress_callback: (data) => {
if (data.status === 'progress') {
const percent = Math.round(data.progress || 0);
statusText.textContent = `Loading Model... ${percent}%`;
}
}
});
// Update UI to ready
modelStatus.classList.remove('loading');
modelStatus.classList.add('ready');
statusText.textContent = "Model Ready";
showToast("Model loaded successfully!", "success");
checkReady();
} catch (error) {
console.error(error);
statusText.textContent = "Error Loading Model";
showToast("Failed to load AI model.", "error");
}
}
// --- 2. File Handling ---
function handleFiles(files) {
selectedFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
if (selectedFiles.length > 0) {
fileCount.textContent = `${selectedFiles.length} files ready`;
dropZone.style.borderColor = 'var(--primary-color)';
} else {
fileCount.textContent = "0 files selected";
dropZone.style.borderColor = 'var(--border-color)';
}
checkReady();
}
fileInput.addEventListener('change', (e) => handleFiles(e.target.files));
// 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');
handleFiles(e.dataTransfer.files);
});
// --- 3. Interaction Logic ---
function checkReady() {
const text = textInput.value.trim();
const hasFiles = selectedFiles.length > 0;
const isModelReady = classifier !== null;
classifyBtn.disabled = !(hasFiles && text && isModelReady);
if (isModelReady) {
if(!hasFiles) classifyBtn.title = "Please upload images";
else if(!text) classifyBtn.title = "Please enter a text prompt";
else classifyBtn.title = "Classify images";
} else {
classifyBtn.title = "Model is still loading...";
}
}
textInput.addEventListener('input', checkReady);
// --- 4. Classification Logic (Fixed) ---
classifyBtn.addEventListener('click', async () => {
const prompt = textInput.value.trim();
if (!classifier || !prompt || selectedFiles.length === 0) return;
// UI Loading State
const originalBtnText = classifyBtn.innerHTML;
classifyBtn.innerHTML = `<div class="spinner"></div> Processing...`;
classifyBtn.disabled = true;
// Prepare results container
resultsContainer.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'results-grid';
resultsContainer.appendChild(grid);
const startTime = performance.now();
const results = [];
try {
// FIX: Process images one by one to avoid "unsupported input type: object"
// Passing an array of File objects directly can sometimes cause issues depending on
// the specific version of transformers.js pre-processor. Looping is safer and
// allows for better progress feedback.
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
// Update status text to show progress
resultsContainer.innerHTML = `<div style="text-align:center; padding: 2rem; color: var(--text-secondary);">Analyzing image ${i + 1} of ${selectedFiles.length}...<br><small>This runs locally on your CPU.</small></div>`;
// Run inference for a single image
// The pipeline expects (image, labels)
const prediction = await classifier(file, [prompt]);
// prediction is usually an array of matches: [{ label: "prompt", score: 0.99 }]
// We take the first (best) match
results.push(prediction[0]);
}
const endTime = performance.now();
const duration = ((endTime - startTime) / 1000).toFixed(2);
renderResults(results, grid);
showToast(`Classified ${selectedFiles.length} images in ${duration}s`, "success");
} catch (error) {
console.error(error);
showToast("An error occurred during classification.", "error");
resultsContainer.innerHTML = `<div class="empty-state"><p style="color: #ef4444">Error: ${error.message}</p></div>`;
} finally {
// Reset Button
classifyBtn.innerHTML = originalBtnText;
checkReady();
}
});
// --- 5. Rendering ---
function renderResults(data, gridElement) {
// Clear the loading text
gridElement.innerHTML = '';
// Create cards
data.forEach((result, index) => {
const score = result.score; // 0 to 1
const percentage = (score * 100).toFixed(1);
const fileUrl = URL.createObjectURL(selectedFiles[index]);
const card = document.createElement('div');
card.className = 'image-card';
// Color coding based on score
let barColor = 'var(--primary-color)';
if(score > 0.8) barColor = 'var(--accent-color)';
else if(score < 0.3) barColor = '#ef4444'; // red
card.innerHTML = `
<div class="card-img-wrapper">
<img src="${fileUrl}" class="card-img" alt="Classified Image" loading="lazy">
</div>
<div class="card-content">
<div class="score-row">
<div class="score-label">
<span>Match Probability</span>
<span style="font-weight:bold; color:${barColor}">${percentage}%</span>
</div>
<div class="score-bar-bg">
<div class="score-bar-fill" style="width: 0%; background: ${barColor}"></div>
</div>
</div>
</div>
`;
gridElement.appendChild(card);
// Trigger animation after append
setTimeout(() => {
card.querySelector('.score-bar-fill').style.width = `${percentage}%`;
}, 50 + (index * 50));
});
resultCount.textContent = `${data.length} images processed`;
}
// --- Utilities ---
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
if (type === 'error') toast.style.borderLeftColor = '#ef4444';
if (type === 'success') toast.style.borderLeftColor = 'var(--accent-color)';
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Initialize
loadModel();
</script>
</body>
</html>