HAV0X1014's picture
WORK YOU STUPID THING!!!
e3e90b2 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kemono Friends Sorter</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: 'Segoe UI', sans-serif; background-color: #f3f4f6; }
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
/* Grid for results */
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
border-radius: 0.75rem;
padding: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.card:hover { transform: translateY(-2px); }
.card-img {
width: 100%;
height: 200px;
object-fit: cover; /* or contain */
border-radius: 0.5rem;
margin-bottom: 1rem;
background-color: #eee;
}
.bar-bg { background-color: #e5e7eb; height: 8px; border-radius: 4px; width: 100%; overflow: hidden; margin-top: 2px;}
.bar-fill { height: 100%; transition: width 0.3s ease; }
/* Color coding labels */
.label-KF { background-color: #16a34a; } /* Green */
.label-NonKF { background-color: #f59e0b; } /* Orange */
.label-Rejected { background-color: #dc2626; } /* Red */
</style>
</head>
<body>
<div class="container">
<div class="bg-white p-6 rounded-xl shadow-lg mb-8">
<h1 class="text-3xl font-bold mb-2 text-gray-800">Kemono Friends Sorter</h1>
<p class="text-sm text-gray-500 mb-4">Batch processing with WebGPU. All data stays in your browser. Sorts images between KF, NonKF, and Rejected, and allows you to download the results.</p>
<!-- Control Panel -->
<div class="space-y-4">
<!-- Status -->
<div id="status" class="text-sm font-semibold text-blue-600 animate-pulse">
Loading model...
</div>
<!-- File Input -->
<div class="flex flex-col md:flex-row gap-4 items-center">
<input type="file" id="file-upload" accept="image/*" multiple disabled
class="block w-full text-sm text-slate-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-orange-50 file:text-orange-700
hover:file:bg-orange-100
cursor-pointer disabled:opacity-50"
/>
<button id="run-btn" disabled
class="px-6 py-2 bg-blue-600 text-white font-semibold rounded-full shadow hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed">
Run Inference
</button>
<button id="download-btn" disabled style="display:none;"
class="px-6 py-2 bg-green-600 text-white font-semibold rounded-full shadow hover:bg-green-700 transition flex items-center gap-2">
<span>⬇️</span> Download Results
</button>
</div>
<div id="file-count" class="text-sm text-gray-500 hidden">0 files selected</div>
</div>
</div>
<!-- Results Area -->
<div id="results-area" class="results-grid">
<!-- Cards injected here -->
</div>
</div>
<script type="module">
import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers';
const MODEL_ID = 'HAV0X1014/ONNX-KF-Sorter';
// DOM Elements
const statusEl = document.getElementById('status');
const fileInput = document.getElementById('file-upload');
const runBtn = document.getElementById('run-btn');
const downloadBtn = document.getElementById('download-btn');
const resultsArea = document.getElementById('results-area');
const fileCountEl = document.getElementById('file-count');
let classifier = null;
let selectedFiles = [];
let processingResults = []; // Stores { filename, label } for download
// 1. Initialize Model
async function init() {
try {
// Force download and cache model
classifier = await pipeline('image-classification', MODEL_ID, { device: "webgpu" },);
statusEl.textContent = 'Model Loaded';
statusEl.classList.remove('text-blue-600', 'animate-pulse');
statusEl.classList.add('text-green-600');
fileInput.disabled = false;
} catch (err) {
statusEl.textContent = 'Error loading model. Check console.';
console.error(err);
}
}
// 2. Handle File Selection
fileInput.addEventListener('change', (e) => {
selectedFiles = Array.from(e.target.files);
if (selectedFiles.length > 0) {
fileCountEl.textContent = `${selectedFiles.length} file(s) selected`;
fileCountEl.classList.remove('hidden');
runBtn.disabled = false;
} else {
fileCountEl.classList.add('hidden');
runBtn.disabled = true;
}
// Hide download button if new files selected (state reset)
downloadBtn.style.display = 'none';
resultsArea.innerHTML = '';
processingResults = [];
});
// 3. Run Inference
runBtn.addEventListener('click', async () => {
if (!classifier || selectedFiles.length === 0) return;
runBtn.disabled = true;
fileInput.disabled = true;
resultsArea.innerHTML = '';
processingResults = []; // Reset download data
let processedCount = 0;
// Process files sequentially
for (const file of selectedFiles) {
statusEl.textContent = `Processing ${processedCount + 1}/${selectedFiles.length}: ${file.name}`;
try {
// Read file to DataURL
const imageUrl = await readFileAsDataURL(file);
// Run Prediction (Top 3)
const output = await classifier(imageUrl, { topk: 3 });
// Render Card
createResultCard(file.name, imageUrl, output);
// Add to results array for download (Top label)
const topResult = output[0]; // Assuming sorted by score
processingResults.push(`${file.name}|${topResult.label}`);
} catch (err) {
console.error(`Error processing ${file.name}`, err);
// Render error card
const errDiv = document.createElement('div');
errDiv.className = 'card text-red-500';
errDiv.innerText = `Error processing ${file.name}`;
resultsArea.appendChild(errDiv);
}
processedCount++;
}
statusEl.textContent = 'Processing Complete';
runBtn.disabled = false;
fileInput.disabled = false;
downloadBtn.style.display = 'flex';
downloadBtn.disabled = false;
});
// 4. Create UI Card
function createResultCard(filename, imgUrl, predictions) {
// Find top label for border color or highlighting (optional)
const topLabel = predictions[0].label;
let statsHtml = '';
predictions.forEach(p => {
const percent = (p.score * 100).toFixed(1);
// Dynamic color based on label name matching CSS classes
const colorClass = `label-${p.label}`;
statsHtml += `
<div class="mb-2">
<div class="flex justify-between text-xs font-semibold text-gray-700">
<span>${p.label}</span>
<span>${percent}%</span>
</div>
<div class="bar-bg">
<div class="bar-fill ${colorClass}" style="width: ${percent}%;"></div>
</div>
</div>
`;
});
const card = document.createElement('div');
card.className = 'card fade-in';
card.innerHTML = `
<img src="${imgUrl}" class="card-img" alt="${filename}">
<div class="text-xs text-gray-400 mb-2 truncate" title="${filename}">${filename}</div>
<div class="font-bold text-lg mb-2 text-gray-800 border-b pb-1">${topLabel}</div>
<div>${statsHtml}</div>
`;
resultsArea.appendChild(card);
}
// 5. Download Handler
downloadBtn.addEventListener('click', () => {
if (processingResults.length === 0) return;
const content = processingResults.join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'results.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Helper: Promisified FileReader
function readFileAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Start
init();
</script>
</body>
</html>