iconclass-predictions / index.html
davanstrien's picture
davanstrien HF Staff
Improve UI with Tufte-inspired design principles
a21dde3
raw
history blame
13 kB
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ICONCLASS Model Evaluation - davanstrien/iconclass-vlm</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
background: white;
padding: 20px;
line-height: 1.5;
color: #333;
font-size: 14px;
}
.header {
max-width: 800px;
margin: 0 auto 30px;
padding: 0;
border-bottom: 1px solid #e5e5e5;
padding-bottom: 20px;
}
h1 {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0 0 8px 0;
}
.subtitle {
font-size: 14px;
color: #666;
margin: 0 0 12px 0;
line-height: 1.4;
}
.subtitle a {
color: #0066cc;
text-decoration: none;
}
.subtitle a:hover {
text-decoration: underline;
}
.description {
font-size: 13px;
color: #666;
margin: 0 0 8px 0;
line-height: 1.5;
}
.stats {
font-size: 11px;
color: #999;
margin: 0;
}
.gallery {
max-width: 800px;
margin: 0 auto;
}
.card {
background: white;
border: 1px solid #e5e5e5;
border-radius: 2px;
overflow: hidden;
margin-bottom: 20px;
}
.card img {
width: 100%;
height: auto;
max-height: 500px;
object-fit: contain;
display: block;
border-bottom: 1px solid #e5e5e5;
background: #fafafa;
}
.card-content {
padding: 15px;
}
.raw-toggle {
font-size: 11px;
color: #999;
cursor: pointer;
margin-bottom: 12px;
user-select: none;
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
}
.raw-toggle:hover {
color: #666;
}
.raw-prediction {
display: none;
background: #fafafa;
padding: 8px;
border: 1px solid #e5e5e5;
border-radius: 2px;
font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
font-size: 11px;
color: #666;
margin-bottom: 15px;
word-break: break-all;
line-height: 1.4;
}
.raw-prediction.visible {
display: block;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 0;
}
.column {
font-size: 13px;
}
.column-title {
font-weight: 400;
margin-bottom: 8px;
color: #999;
font-size: 10px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.label {
padding: 3px 0 3px 6px;
margin: 2px 0;
position: relative;
border-left: 2px solid transparent;
font-size: 13px;
line-height: 1.4;
}
.label:hover {
background: #fafafa;
}
/* Prediction styles */
.prediction {
color: #333;
}
.prediction.invalid {
color: #999;
text-decoration: line-through;
}
.prediction.match {
border-left-color: #4caf50;
font-weight: 500;
}
/* Ground truth styles */
.ground-truth {
color: #333;
}
.ground-truth.matched {
border-left-color: #4caf50;
font-weight: 500;
}
/* Match statistics */
.match-stats {
font-size: 11px;
color: #999;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
.controls {
text-align: center;
margin: 30px 0;
padding-top: 20px;
}
.load-more {
color: #0066cc;
text-decoration: none;
font-size: 13px;
cursor: pointer;
background: none;
border: none;
padding: 0;
font-family: inherit;
}
.load-more:hover {
text-decoration: underline;
}
.load-more:disabled {
color: #999;
cursor: default;
text-decoration: none;
}
.loading {
text-align: center;
padding: 10px;
color: #999;
font-size: 12px;
}
.loading.hidden {
display: none;
}
</style>
</head>
<body>
<div class="header">
<h1>ICONCLASS Model Evaluation</h1>
<div class="subtitle">
Comparing predictions from <a href="https://huggingface.co/davanstrien/iconclass-vlm" target="_blank">davanstrien/iconclass-vlm</a> against ground truth labels
</div>
<div class="description">
A vision-language model fine-tuned on Qwen2.5-VL-3B for classifying art and cultural heritage images using ICONCLASS notation — a hierarchical classification system for art and iconography.
</div>
<div class="stats">
Showing <span id="loadedCount">0</span> of <span id="totalCount">-</span> test images
</div>
</div>
<div id="gallery" class="gallery"></div>
<div class="loading hidden" id="loading">Loading...</div>
<div class="controls">
<button id="loadMore" class="load-more">Load more images</button>
</div>
<script>
// Configuration
const DATASET = "davanstrien/iconclass-sft-predictions";
const CONFIG = "default";
const SPLIT = "test";
const PAGE_SIZE = 10;
// State
let currentOffset = 0;
let totalRows = null;
let isLoading = false;
async function loadDatasetPage() {
if (isLoading) return;
isLoading = true;
const loadingDiv = document.getElementById("loading");
loadingDiv.classList.remove("hidden");
try {
const response = await fetch(
`https://datasets-server.huggingface.co/rows?dataset=${DATASET}&config=${CONFIG}&split=${SPLIT}&offset=${currentOffset}&length=${PAGE_SIZE}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Update stats
if (data.num_rows_total) {
totalRows = data.num_rows_total;
document.getElementById("totalCount").textContent = totalRows;
}
// Display rows
displayRows(data.rows);
// Update counter
currentOffset += data.rows.length;
document.getElementById("loadedCount").textContent = currentOffset;
// Update button
const loadMoreBtn = document.getElementById("loadMore");
if (currentOffset >= totalRows) {
loadMoreBtn.disabled = true;
loadMoreBtn.textContent = "All Images Loaded";
} else {
loadMoreBtn.textContent = `Load more images (${
totalRows - currentOffset
} remaining)`;
}
} catch (error) {
console.error("Error:", error);
} finally {
isLoading = false;
loadingDiv.classList.add("hidden");
}
}
function displayRows(rows) {
const gallery = document.getElementById("gallery");
rows.forEach((item) => {
const row = item.row;
// Create card
const card = document.createElement("div");
card.className = "card";
// Add image
if (row.images && row.images.length > 0) {
const img = document.createElement("img");
img.src = row.images[0].src;
img.loading = "lazy";
card.appendChild(img);
}
// Create content
const content = document.createElement("div");
content.className = "card-content";
// Show raw prediction (collapsible)
if (row["iconclass-prediction"]) {
const toggleDiv = document.createElement("div");
toggleDiv.className = "raw-toggle";
toggleDiv.textContent = "+ Show raw prediction";
const rawDiv = document.createElement("div");
rawDiv.className = "raw-prediction";
rawDiv.textContent = row["iconclass-prediction"];
toggleDiv.addEventListener("click", () => {
if (rawDiv.classList.contains("visible")) {
rawDiv.classList.remove("visible");
toggleDiv.textContent = "+ Show raw prediction";
} else {
rawDiv.classList.add("visible");
toggleDiv.textContent = "− Hide raw prediction";
}
});
content.appendChild(toggleDiv);
content.appendChild(rawDiv);
}
// Parse predictions and ground truth
const predictions = row["iconclass-predictions-parsed"] || [];
const groundTruth = row["iconclass-gt-parsed"] || [];
// Check for invalid labels (simple heuristic)
const invalidPredictions = predictions.map((pred) => {
// If it says "Not a valid iconclass label" or similar
return (
pred.toLowerCase().includes("not a valid") ||
pred.toLowerCase().includes("invalid")
);
});
// Find matches
const matches = predictions.filter((pred) =>
groundTruth.some(
(gt) =>
gt.toLowerCase().includes(pred.toLowerCase()) ||
pred.toLowerCase().includes(gt.toLowerCase())
)
);
// Create comparison view
const comparison = document.createElement("div");
comparison.className = "comparison";
// Predictions column
const predColumn = document.createElement("div");
predColumn.className = "column";
const predTitle = document.createElement("div");
predTitle.className = "column-title";
predTitle.textContent = "Predictions";
predColumn.appendChild(predTitle);
predictions.forEach((pred, idx) => {
const label = document.createElement("div");
const isInvalid = invalidPredictions[idx];
const isMatch = matches.includes(pred);
label.className = `label prediction ${isInvalid ? "invalid" : ""} ${
isMatch && !isInvalid ? "match" : ""
}`;
label.textContent = pred;
predColumn.appendChild(label);
});
// Ground truth column
const gtColumn = document.createElement("div");
gtColumn.className = "column";
const gtTitle = document.createElement("div");
gtTitle.className = "column-title";
gtTitle.textContent = "Ground Truth";
gtColumn.appendChild(gtTitle);
groundTruth.forEach((gt) => {
const label = document.createElement("div");
const isMatched = matches.some(
(pred) =>
gt.toLowerCase().includes(pred.toLowerCase()) ||
pred.toLowerCase().includes(gt.toLowerCase())
);
label.className = `label ground-truth ${
isMatched ? "matched" : ""
}`;
label.textContent = gt;
gtColumn.appendChild(label);
});
comparison.appendChild(predColumn);
comparison.appendChild(gtColumn);
content.appendChild(comparison);
// Add match statistics
const validPredictions = predictions.filter(
(_, idx) => !invalidPredictions[idx]
);
const matchScore =
validPredictions.length > 0
? Math.round((matches.length / validPredictions.length) * 100)
: 0;
const statsDiv = document.createElement("div");
statsDiv.className = "match-stats";
statsDiv.textContent = validPredictions.length > 0
? `${matches.length}/${validPredictions.length} matches`
: `No valid predictions`;
content.appendChild(statsDiv);
card.appendChild(content);
gallery.appendChild(card);
});
}
// Event listeners
document
.getElementById("loadMore")
.addEventListener("click", loadDatasetPage);
// Infinite scroll
window.addEventListener("scroll", () => {
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 100
) {
if (!isLoading && currentOffset < totalRows) {
loadDatasetPage();
}
}
});
// Load first page
loadDatasetPage();
</script>
</body>
</html>