Spaces:
Running
Running
Upload 2 files
Browse filesFixed issue with Q&A
- README.md +15 -5
- index.html +111 -32
README.md
CHANGED
|
@@ -8,7 +8,7 @@ pinned: false
|
|
| 8 |
license: mit
|
| 9 |
models:
|
| 10 |
- Xenova/vit-gpt2-image-captioning
|
| 11 |
-
- Xenova/
|
| 12 |
- Xenova/clip-vit-base-patch32
|
| 13 |
- Xenova/trocr-small-printed
|
| 14 |
---
|
|
@@ -28,7 +28,7 @@ acceleration when the browser supports it.
|
|
| 28 |
| Tab | Task | Model |
|
| 29 |
|---|---|---|
|
| 30 |
| Caption | Image → natural-language description | `Xenova/vit-gpt2-image-captioning` |
|
| 31 |
-
|
|
| 32 |
| Classify | Zero-shot classification against any labels you define | `Xenova/clip-vit-base-patch32` |
|
| 33 |
| Read Text | OCR on printed text in an image | `Xenova/trocr-small-printed` |
|
| 34 |
| Semantic Search | Rank a gallery of images against a natural-language query | `Xenova/clip-vit-base-patch32` (shared) |
|
|
@@ -66,6 +66,16 @@ python -m http.server 8000
|
|
| 66 |
## Extending it
|
| 67 |
|
| 68 |
Ideas for going further: add a webcam capture option alongside file upload,
|
| 69 |
-
swap in a larger
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
license: mit
|
| 9 |
models:
|
| 10 |
- Xenova/vit-gpt2-image-captioning
|
| 11 |
+
- Xenova/owlvit-base-patch32
|
| 12 |
- Xenova/clip-vit-base-patch32
|
| 13 |
- Xenova/trocr-small-printed
|
| 14 |
---
|
|
|
|
| 28 |
| Tab | Task | Model |
|
| 29 |
|---|---|---|
|
| 30 |
| Caption | Image → natural-language description | `Xenova/vit-gpt2-image-captioning` |
|
| 31 |
+
| Detect Objects | Draws bounding boxes for any labels you define, even ones the model wasn't explicitly trained to detect | `Xenova/owlvit-base-patch32` |
|
| 32 |
| Classify | Zero-shot classification against any labels you define | `Xenova/clip-vit-base-patch32` |
|
| 33 |
| Read Text | OCR on printed text in an image | `Xenova/trocr-small-printed` |
|
| 34 |
| Semantic Search | Rank a gallery of images against a natural-language query | `Xenova/clip-vit-base-patch32` (shared) |
|
|
|
|
| 66 |
## Extending it
|
| 67 |
|
| 68 |
Ideas for going further: add a webcam capture option alongside file upload,
|
| 69 |
+
swap in a larger captioning model for better quality (trading off download
|
| 70 |
+
size), or add an "export gallery as JSON" button to persist search results
|
| 71 |
+
across sessions.
|
| 72 |
+
|
| 73 |
+
## A note on transformers.js task coverage
|
| 74 |
+
|
| 75 |
+
Not every task in the Python `transformers` library has a browser
|
| 76 |
+
equivalent. Visual question answering, for instance, isn't currently a
|
| 77 |
+
supported transformers.js pipeline — only its listed NLP, vision, audio,
|
| 78 |
+
and multimodal tasks (embeddings, zero-shot classification/detection) run
|
| 79 |
+
client-side. Object detection was used here instead of VQA for exactly that
|
| 80 |
+
reason: it's a confirmed, supported multimodal task that still combines
|
| 81 |
+
free-form text labels with an image.
|
index.html
CHANGED
|
@@ -301,6 +301,32 @@
|
|
| 301 |
100% { top: 4%; }
|
| 302 |
}
|
| 303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
.vf-replace {
|
| 305 |
position: absolute;
|
| 306 |
bottom: 10px;
|
|
@@ -599,7 +625,7 @@
|
|
| 599 |
<div class="layout">
|
| 600 |
<nav class="tabs" id="tabNav">
|
| 601 |
<button class="tab-btn active" data-tab="caption"><span class="idx">01</span> Caption</button>
|
| 602 |
-
<button class="tab-btn" data-tab="
|
| 603 |
<button class="tab-btn" data-tab="classify"><span class="idx">03</span> Classify</button>
|
| 604 |
<button class="tab-btn" data-tab="ocr"><span class="idx">04</span> Read Text</button>
|
| 605 |
<button class="tab-btn" data-tab="search"><span class="idx">05</span> Semantic Search</button>
|
|
@@ -622,6 +648,7 @@
|
|
| 622 |
<div class="bracket bl"></div>
|
| 623 |
<div class="bracket br"></div>
|
| 624 |
<div class="scanline" id="scanline"></div>
|
|
|
|
| 625 |
<div class="vf-replace">click to replace</div>
|
| 626 |
</div>
|
| 627 |
</div>
|
|
@@ -645,21 +672,21 @@
|
|
| 645 |
</div>
|
| 646 |
</section>
|
| 647 |
|
| 648 |
-
<!-- Panel:
|
| 649 |
-
<section class="panel" data-panel="
|
| 650 |
<div class="panel-head">
|
| 651 |
-
<h2>
|
| 652 |
-
<p>
|
| 653 |
-
<span class="model-tag">model: Xenova/
|
| 654 |
</div>
|
| 655 |
<div class="controls">
|
| 656 |
-
<input type="text" id="
|
| 657 |
-
<button class="run" id="
|
| 658 |
</div>
|
| 659 |
<div class="output">
|
| 660 |
-
<div class="output-card" id="
|
| 661 |
-
<div class="output-label">
|
| 662 |
-
<div
|
| 663 |
</div>
|
| 664 |
</div>
|
| 665 |
</section>
|
|
@@ -795,7 +822,7 @@
|
|
| 795 |
/* ---------------- Model registry ---------------- */
|
| 796 |
const MODEL_SPECS = {
|
| 797 |
caption: { task: "image-to-text", model: "Xenova/vit-gpt2-image-captioning" },
|
| 798 |
-
|
| 799 |
classify: { task: "zero-shot-image-classification", model: "Xenova/clip-vit-base-patch32" },
|
| 800 |
ocr: { task: "image-to-text", model: "Xenova/trocr-small-printed" },
|
| 801 |
};
|
|
@@ -865,7 +892,7 @@
|
|
| 865 |
let currentImageURL = null;
|
| 866 |
const actionButtons = [
|
| 867 |
document.getElementById("btnCaption"),
|
| 868 |
-
document.getElementById("
|
| 869 |
document.getElementById("btnClassify"),
|
| 870 |
document.getElementById("btnOcr"),
|
| 871 |
];
|
|
@@ -876,6 +903,7 @@
|
|
| 876 |
vfEmpty.style.display = "none";
|
| 877 |
vfImageWrap.style.display = "block";
|
| 878 |
actionButtons.forEach((b) => (b.disabled = false));
|
|
|
|
| 879 |
log("Image loaded into viewfinder");
|
| 880 |
}
|
| 881 |
|
|
@@ -943,31 +971,82 @@
|
|
| 943 |
}
|
| 944 |
});
|
| 945 |
|
| 946 |
-
/* ----------------
|
| 947 |
-
const
|
| 948 |
-
const
|
| 949 |
-
const
|
| 950 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
|
| 952 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 953 |
if (!currentImageURL) return;
|
| 954 |
-
const
|
| 955 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
withScan(true);
|
| 957 |
-
setBusy(true, "
|
| 958 |
try {
|
| 959 |
-
const
|
| 960 |
-
const
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
} catch (err) {
|
| 966 |
-
|
| 967 |
-
outVqa.classList.add("show");
|
| 968 |
-
log(`VQA error: ${err.message || err}`, "err");
|
| 969 |
} finally {
|
| 970 |
-
|
| 971 |
withScan(false);
|
| 972 |
setBusy(false);
|
| 973 |
}
|
|
|
|
| 301 |
100% { top: 4%; }
|
| 302 |
}
|
| 303 |
|
| 304 |
+
.detect-boxes {
|
| 305 |
+
position: absolute;
|
| 306 |
+
inset: 0;
|
| 307 |
+
pointer-events: none;
|
| 308 |
+
}
|
| 309 |
+
.detect-box {
|
| 310 |
+
position: absolute;
|
| 311 |
+
border: 2px solid var(--coral);
|
| 312 |
+
box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
|
| 313 |
+
border-radius: 2px;
|
| 314 |
+
}
|
| 315 |
+
.detect-box .tag {
|
| 316 |
+
position: absolute;
|
| 317 |
+
top: -20px;
|
| 318 |
+
left: -2px;
|
| 319 |
+
background: var(--coral);
|
| 320 |
+
color: #1a0a06;
|
| 321 |
+
font-family: var(--mono);
|
| 322 |
+
font-size: 10px;
|
| 323 |
+
font-weight: 600;
|
| 324 |
+
padding: 1px 6px;
|
| 325 |
+
border-radius: 3px;
|
| 326 |
+
white-space: nowrap;
|
| 327 |
+
}
|
| 328 |
+
.detect-box.tag-inside .tag { top: 2px; left: 2px; }
|
| 329 |
+
|
| 330 |
.vf-replace {
|
| 331 |
position: absolute;
|
| 332 |
bottom: 10px;
|
|
|
|
| 625 |
<div class="layout">
|
| 626 |
<nav class="tabs" id="tabNav">
|
| 627 |
<button class="tab-btn active" data-tab="caption"><span class="idx">01</span> Caption</button>
|
| 628 |
+
<button class="tab-btn" data-tab="detect"><span class="idx">02</span> Detect Objects</button>
|
| 629 |
<button class="tab-btn" data-tab="classify"><span class="idx">03</span> Classify</button>
|
| 630 |
<button class="tab-btn" data-tab="ocr"><span class="idx">04</span> Read Text</button>
|
| 631 |
<button class="tab-btn" data-tab="search"><span class="idx">05</span> Semantic Search</button>
|
|
|
|
| 648 |
<div class="bracket bl"></div>
|
| 649 |
<div class="bracket br"></div>
|
| 650 |
<div class="scanline" id="scanline"></div>
|
| 651 |
+
<div class="detect-boxes" id="detectBoxes"></div>
|
| 652 |
<div class="vf-replace">click to replace</div>
|
| 653 |
</div>
|
| 654 |
</div>
|
|
|
|
| 672 |
</div>
|
| 673 |
</section>
|
| 674 |
|
| 675 |
+
<!-- Panel: Detect -->
|
| 676 |
+
<section class="panel" data-panel="detect">
|
| 677 |
<div class="panel-head">
|
| 678 |
+
<h2>Zero-Shot Object Detection</h2>
|
| 679 |
+
<p>Define any set of objects to look for — the model locates and draws bounding boxes around them in the image above, even for categories it wasn't explicitly trained to detect.</p>
|
| 680 |
+
<span class="model-tag">model: Xenova/owlvit-base-patch32</span>
|
| 681 |
</div>
|
| 682 |
<div class="controls">
|
| 683 |
+
<input type="text" id="detectLabels" placeholder="person, dog, car, tree, building" value="person, dog, car, tree, building" />
|
| 684 |
+
<button class="run" id="btnDetect" disabled>Detect objects</button>
|
| 685 |
</div>
|
| 686 |
<div class="output">
|
| 687 |
+
<div class="output-card" id="outDetect">
|
| 688 |
+
<div class="output-label">Detections</div>
|
| 689 |
+
<div id="detectList"></div>
|
| 690 |
</div>
|
| 691 |
</div>
|
| 692 |
</section>
|
|
|
|
| 822 |
/* ---------------- Model registry ---------------- */
|
| 823 |
const MODEL_SPECS = {
|
| 824 |
caption: { task: "image-to-text", model: "Xenova/vit-gpt2-image-captioning" },
|
| 825 |
+
detect: { task: "zero-shot-object-detection", model: "Xenova/owlvit-base-patch32" },
|
| 826 |
classify: { task: "zero-shot-image-classification", model: "Xenova/clip-vit-base-patch32" },
|
| 827 |
ocr: { task: "image-to-text", model: "Xenova/trocr-small-printed" },
|
| 828 |
};
|
|
|
|
| 892 |
let currentImageURL = null;
|
| 893 |
const actionButtons = [
|
| 894 |
document.getElementById("btnCaption"),
|
| 895 |
+
document.getElementById("btnDetect"),
|
| 896 |
document.getElementById("btnClassify"),
|
| 897 |
document.getElementById("btnOcr"),
|
| 898 |
];
|
|
|
|
| 903 |
vfEmpty.style.display = "none";
|
| 904 |
vfImageWrap.style.display = "block";
|
| 905 |
actionButtons.forEach((b) => (b.disabled = false));
|
| 906 |
+
clearDetectionBoxes();
|
| 907 |
log("Image loaded into viewfinder");
|
| 908 |
}
|
| 909 |
|
|
|
|
| 971 |
}
|
| 972 |
});
|
| 973 |
|
| 974 |
+
/* ---------------- Detect (zero-shot object detection) ---------------- */
|
| 975 |
+
const btnDetect = document.getElementById("btnDetect");
|
| 976 |
+
const detectLabels = document.getElementById("detectLabels");
|
| 977 |
+
const outDetect = document.getElementById("outDetect");
|
| 978 |
+
const detectList = document.getElementById("detectList");
|
| 979 |
+
const detectBoxesEl = document.getElementById("detectBoxes");
|
| 980 |
+
let lastDetections = [];
|
| 981 |
+
|
| 982 |
+
function clearDetectionBoxes() {
|
| 983 |
+
lastDetections = [];
|
| 984 |
+
detectBoxesEl.innerHTML = "";
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
function renderDetectionBoxes() {
|
| 988 |
+
detectBoxesEl.innerHTML = "";
|
| 989 |
+
if (!lastDetections.length || !vfImage.naturalWidth) return;
|
| 990 |
+
const containerW = vfImageWrap.clientWidth;
|
| 991 |
+
const containerH = vfImageWrap.clientHeight;
|
| 992 |
+
const naturalW = vfImage.naturalWidth;
|
| 993 |
+
const naturalH = vfImage.naturalHeight;
|
| 994 |
+
const scale = Math.min(containerW / naturalW, containerH / naturalH);
|
| 995 |
+
const renderW = naturalW * scale;
|
| 996 |
+
const renderH = naturalH * scale;
|
| 997 |
+
const offsetX = (containerW - renderW) / 2;
|
| 998 |
+
const offsetY = (containerH - renderH) / 2;
|
| 999 |
+
|
| 1000 |
+
lastDetections.forEach((d) => {
|
| 1001 |
+
const box = document.createElement("div");
|
| 1002 |
+
const top = offsetY + d.box.ymin * scale;
|
| 1003 |
+
box.className = "detect-box" + (top < 22 ? " tag-inside" : "");
|
| 1004 |
+
box.style.left = (offsetX + d.box.xmin * scale) + "px";
|
| 1005 |
+
box.style.top = top + "px";
|
| 1006 |
+
box.style.width = ((d.box.xmax - d.box.xmin) * scale) + "px";
|
| 1007 |
+
box.style.height = ((d.box.ymax - d.box.ymin) * scale) + "px";
|
| 1008 |
+
const tag = document.createElement("div");
|
| 1009 |
+
tag.className = "tag";
|
| 1010 |
+
tag.textContent = `${d.label} ${(d.score * 100).toFixed(0)}%`;
|
| 1011 |
+
box.appendChild(tag);
|
| 1012 |
+
detectBoxesEl.appendChild(box);
|
| 1013 |
+
});
|
| 1014 |
+
}
|
| 1015 |
|
| 1016 |
+
let resizeTimer;
|
| 1017 |
+
window.addEventListener("resize", () => {
|
| 1018 |
+
clearTimeout(resizeTimer);
|
| 1019 |
+
resizeTimer = setTimeout(renderDetectionBoxes, 120);
|
| 1020 |
+
});
|
| 1021 |
+
|
| 1022 |
+
btnDetect.addEventListener("click", async () => {
|
| 1023 |
if (!currentImageURL) return;
|
| 1024 |
+
const labels = detectLabels.value.split(",").map((s) => s.trim()).filter(Boolean);
|
| 1025 |
+
if (!labels.length) {
|
| 1026 |
+
log("Enter at least one label to detect", "warn");
|
| 1027 |
+
return;
|
| 1028 |
+
}
|
| 1029 |
+
btnDetect.disabled = true;
|
| 1030 |
withScan(true);
|
| 1031 |
+
setBusy(true, "detecting objects…");
|
| 1032 |
try {
|
| 1033 |
+
const detector = await getPipeline("detect");
|
| 1034 |
+
const output = await detector(currentImageURL, labels, { threshold: 0.12, top_k: 12 });
|
| 1035 |
+
lastDetections = output;
|
| 1036 |
+
renderDetectionBoxes();
|
| 1037 |
+
|
| 1038 |
+
detectList.innerHTML = "";
|
| 1039 |
+
if (!output.length) {
|
| 1040 |
+
detectList.innerHTML = `<div class="output-text" style="font-size:14px; color: var(--ink-dim);">No objects matched those labels above the confidence threshold.</div>`;
|
| 1041 |
+
} else {
|
| 1042 |
+
renderBars(detectList, [...output].sort((a, b) => b.score - a.score));
|
| 1043 |
+
}
|
| 1044 |
+
outDetect.classList.add("show");
|
| 1045 |
+
log(`Detected ${output.length} object(s) across ${labels.length} label(s)`, "ok");
|
| 1046 |
} catch (err) {
|
| 1047 |
+
log(`Detect error: ${err.message || err}`, "err");
|
|
|
|
|
|
|
| 1048 |
} finally {
|
| 1049 |
+
btnDetect.disabled = false;
|
| 1050 |
withScan(false);
|
| 1051 |
setBusy(false);
|
| 1052 |
}
|