Spaces:
Running on Zero
Running on Zero
Commit ·
b137257
0
Parent(s):
document-classification-demo — open-vocab, zero-shot Gradio demo
Browse files- .gitattributes +38 -0
- README.md +33 -0
- app.py +235 -0
- examples/bar-chart_1.png +0 -0
- examples/financial-reports_1.png +0 -0
- examples/financial-reports_2.png +3 -0
- examples/manuals_1.png +3 -0
- examples/manuals_2.png +0 -0
- examples/pie-chart_1.png +0 -0
- examples/scientific-articles_1.png +0 -0
- examples/scientific-articles_2.png +3 -0
- examples/screenshot_1.png +0 -0
- examples/signature_1.png +0 -0
- requirements.txt +9 -0
.gitattributes
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/financial-reports_2.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
examples/manuals_1.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/scientific-articles_2.png filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Document Classification Demo
|
| 3 |
+
emoji: 🖼️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.20.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: other
|
| 11 |
+
short_description: Classify any document image against labels you choose
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Nutrient document classification demo
|
| 15 |
+
|
| 16 |
+
Open-vocabulary, zero-shot: add candidate classes (a **label** + optional **description** per row), upload a
|
| 17 |
+
document page, and the model ranks them — no fixed class list, no per-class training. Two models, one tab each:
|
| 18 |
+
|
| 19 |
+
- **v2 flagship** — the commercial model (best accuracy).
|
| 20 |
+
- **v1 (open-weight)** — a downloadable open-vocab classifier.
|
| 21 |
+
|
| 22 |
+
→ [model](https://huggingface.co/nutrientdocs/document-classification-v2) ·
|
| 23 |
+
[leaderboard](https://huggingface.co/spaces/nutrientdocs/document-classification-leaderboard) ·
|
| 24 |
+
[benchmark](https://huggingface.co/datasets/nutrientdocs/document-classification-benchmark)
|
| 25 |
+
|
| 26 |
+
## Secret required
|
| 27 |
+
|
| 28 |
+
The **v2** model is commercial and loaded from a **private** repo, so this Space needs an `HF_TOKEN`
|
| 29 |
+
**secret** (Settings → Variables and secrets) with read access to it. The weights are used server-side only
|
| 30 |
+
and are never downloadable. Without the secret, the demo marks the v2 tab _unavailable_ and the open **v1**
|
| 31 |
+
model still runs. Inference runs on **ZeroGPU**.
|
| 32 |
+
|
| 33 |
+
Sample document images for quick testing are in `examples/`.
|
app.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Nutrient Document Classification — open-vocab, zero-shot demo (public-ready).
|
| 2 |
+
|
| 3 |
+
Censored like the sibling grounding demo: NO proprietary weights and NO custom architecture code ship in this
|
| 4 |
+
Space. Everything trained runs as ONNX loaded at RUNTIME:
|
| 5 |
+
* v2 flagship (commercial) — the full model runs as ONNX fetched from the PRIVATE repo via the
|
| 6 |
+
HF_TOKEN secret (server-side only, never committed here). Marked unavailable if the secret is absent.
|
| 7 |
+
* v1 (open-weight) — ONNX from the public v1 repo.
|
| 8 |
+
The preprocessor + tokenizer are bundled alongside the ONNX in the source repos, so no model identifiers
|
| 9 |
+
appear in this app. Runs on ZeroGPU; ONNX inference on CPU. Results are a ranked Markdown table.
|
| 10 |
+
"""
|
| 11 |
+
import glob as _glob
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import numpy as np
|
| 16 |
+
import spaces
|
| 17 |
+
from huggingface_hub import hf_hub_download
|
| 18 |
+
from transformers import AutoImageProcessor, AutoTokenizer
|
| 19 |
+
|
| 20 |
+
V2_PRIV = os.environ.get("V2_REPO", "nutrientdocs/document-classification-v2-private")
|
| 21 |
+
V1_REPO = os.environ.get("V1_REPO", "nutrientdocs/document-classification-v1")
|
| 22 |
+
TOKEN = os.environ.get("HF_TOKEN")
|
| 23 |
+
|
| 24 |
+
_V2, _V1 = {}, {} # lazy caches: v2 (private) ONNX + processors / v1 (public) ONNX
|
| 25 |
+
|
| 26 |
+
DEFAULT_ROWS = [
|
| 27 |
+
["invoice", "an itemized bill listing goods or services and a total due"],
|
| 28 |
+
["letter", "correspondence with a salutation, body, and signature"],
|
| 29 |
+
["memo", "an internal memorandum headed To, From, Date, Subject"],
|
| 30 |
+
["form", "a structured template with labeled fields to fill in"],
|
| 31 |
+
["scientific article", "a research paper with an abstract, methods, and references"],
|
| 32 |
+
["resume", "a summary of education, work experience, and skills"],
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _rows_to_text(rows):
|
| 37 |
+
return "\n".join(f"{lab} | {desc}" if desc else lab for lab, desc in rows)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
DEFAULT_TEXT = _rows_to_text(DEFAULT_ROWS)
|
| 41 |
+
|
| 42 |
+
# Clickable examples — each loads the page image AND its candidate-label table. Only pages the model
|
| 43 |
+
# classifies correctly are included (verified on the benchmark fixtures).
|
| 44 |
+
_DESC = {
|
| 45 |
+
"financial reports": "a corporate financial statement with balance sheets, income tables, and figures",
|
| 46 |
+
"scientific articles": "a research paper with an abstract, methods, results, and references",
|
| 47 |
+
"laws and regulations": "statutory legal text with numbered sections and articles",
|
| 48 |
+
"government tenders": "a public procurement notice or call for bids",
|
| 49 |
+
"manuals": "a product or technical manual with instructions and labeled diagrams",
|
| 50 |
+
"patents": "a patent document with claims, drawings, and an application number",
|
| 51 |
+
"invoice": "an itemized bill listing goods or services, quantities, prices, and a total due",
|
| 52 |
+
"letter": "correspondence with a salutation, body, and signature",
|
| 53 |
+
"presentation": "a slide from a slideshow presentation", "receipt": "a store receipt with a total",
|
| 54 |
+
"bar chart": "a chart comparing values with rectangular bars",
|
| 55 |
+
"line chart": "a chart showing a trend as a line over an axis",
|
| 56 |
+
"pie chart": "a circular chart divided into proportional slices",
|
| 57 |
+
"qr code": "a square two-dimensional matrix barcode", "bar code": "a striped one-dimensional barcode",
|
| 58 |
+
"signature": "a handwritten signature mark", "screenshot": "a screenshot of a software user interface",
|
| 59 |
+
"table": "a grid of rows and columns of data",
|
| 60 |
+
}
|
| 61 |
+
_DOC = ["financial reports", "scientific articles", "laws and regulations", "government tenders", "manuals",
|
| 62 |
+
"patents", "invoice", "letter", "presentation", "receipt"]
|
| 63 |
+
_ELEM = ["bar chart", "line chart", "pie chart", "qr code", "bar code", "signature", "screenshot", "table",
|
| 64 |
+
"invoice", "financial reports"]
|
| 65 |
+
_ELEM_GT = {"bar chart", "pie chart", "line chart", "signature", "screenshot", "qr code", "bar code", "table"}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _example_set():
|
| 69 |
+
out = []
|
| 70 |
+
for p in sorted(_glob.glob(os.path.join(os.path.dirname(__file__), "examples", "*.png"))):
|
| 71 |
+
gt = os.path.basename(p).rsplit("_", 1)[0].replace("-", " ")
|
| 72 |
+
group = _ELEM if gt in _ELEM_GT else _DOC
|
| 73 |
+
if gt not in group:
|
| 74 |
+
group = group + [gt]
|
| 75 |
+
out.append((gt, p, [[lab, _DESC.get(lab, "")] for lab in group]))
|
| 76 |
+
return out
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
EXAMPLES = _example_set()
|
| 80 |
+
# one clickable example per document type (keeps the row short + each pick unambiguous)
|
| 81 |
+
_EX_BY_NAME = {}
|
| 82 |
+
for _gt, _p, _t in EXAMPLES:
|
| 83 |
+
_EX_BY_NAME.setdefault(_gt, (_p, _t))
|
| 84 |
+
EXAMPLE_NAMES = list(_EX_BY_NAME)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _load_example(name):
|
| 88 |
+
p, t = _EX_BY_NAME.get(name, (None, DEFAULT_ROWS))
|
| 89 |
+
return p, _rows_to_text(t)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _parse(text):
|
| 93 |
+
labels, queries = [], []
|
| 94 |
+
for line in (text or "").splitlines():
|
| 95 |
+
lab, _, desc = line.partition("|")
|
| 96 |
+
lab, desc = lab.strip(), desc.strip()
|
| 97 |
+
if not lab:
|
| 98 |
+
continue
|
| 99 |
+
labels.append(lab); queries.append(f"{lab}. {desc}" if desc else lab)
|
| 100 |
+
return labels, queries
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _table(labels, probs):
|
| 104 |
+
ranked = sorted(zip(labels, probs), key=lambda x: -x[1])
|
| 105 |
+
rows = []
|
| 106 |
+
for i, (lab, p) in enumerate(ranked, 1):
|
| 107 |
+
pct = max(0.0, min(1.0, p)) * 100
|
| 108 |
+
star = " ✅" if i == 1 else ""
|
| 109 |
+
rows.append(
|
| 110 |
+
f"<tr><td style='text-align:right;padding:4px 10px;color:#5c6773'>{i}</td>"
|
| 111 |
+
f"<td style='padding:4px 10px'><b>{lab}</b>{star}</td>"
|
| 112 |
+
f"<td style='padding:4px 10px;width:180px'>"
|
| 113 |
+
f"<div style='background:#e6ecf5;border-radius:4px;height:14px'>"
|
| 114 |
+
f"<div style='background:#2f52d0;height:14px;border-radius:4px;width:{pct:.0f}%'></div></div></td>"
|
| 115 |
+
f"<td style='text-align:right;padding:4px 10px;font-variant-numeric:tabular-nums'>{p:.3f}</td></tr>")
|
| 116 |
+
return ("<table style='border-collapse:collapse;font:14px system-ui'>"
|
| 117 |
+
"<thead><tr>"
|
| 118 |
+
"<th style='text-align:right;padding:4px 10px'>#</th>"
|
| 119 |
+
"<th style='text-align:left;padding:4px 10px'>Label</th>"
|
| 120 |
+
"<th style='padding:4px 10px'></th>"
|
| 121 |
+
"<th style='text-align:right;padding:4px 10px'>Score</th></tr></thead>"
|
| 122 |
+
"<tbody>" + "".join(rows) + "</tbody></table>")
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _ort(path):
|
| 126 |
+
import onnxruntime as ort
|
| 127 |
+
return ort.InferenceSession(path, providers=["CPUExecutionProvider"])
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ---------- v2 flagship: full model as ONNX from the PRIVATE repo (censored) ----------
|
| 131 |
+
def _ensure_v2():
|
| 132 |
+
if _V2:
|
| 133 |
+
return
|
| 134 |
+
if not TOKEN:
|
| 135 |
+
_V2["error"] = "no HF_TOKEN secret"; return
|
| 136 |
+
try:
|
| 137 |
+
dl = lambda f: hf_hub_download(V2_PRIV, f, repo_type="model", token=TOKEN)
|
| 138 |
+
_V2["img"] = _ort(dl("weights/image_encoder.onnx")) # pixel_values -> pooled patches [1,256,1152]
|
| 139 |
+
_V2["txt"] = _ort(dl("weights/text_encoder.onnx")) # input_ids/mask -> L2 label embeds [N,1024]
|
| 140 |
+
_V2["head"] = _ort(dl("weights/head-v3-ep6.onnx")) # patches + label_embeds -> probs
|
| 141 |
+
_V2["ip"] = AutoImageProcessor.from_pretrained(V2_PRIV, subfolder="encoder/image", token=TOKEN)
|
| 142 |
+
_V2["tk"] = AutoTokenizer.from_pretrained(V2_PRIV, subfolder="encoder/text", token=TOKEN)
|
| 143 |
+
except Exception as e: # noqa: BLE001
|
| 144 |
+
_V2["error"] = f"{type(e).__name__}"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _run_v2(image, queries):
|
| 148 |
+
_ensure_v2()
|
| 149 |
+
if "error" in _V2:
|
| 150 |
+
return None, f"⚠️ Flagship unavailable ({_V2['error']}). This model needs the commercial weights."
|
| 151 |
+
pv = _V2["ip"](images=[image.convert("RGB")], return_tensors="np")["pixel_values"].astype(np.float32)
|
| 152 |
+
patches = _V2["img"].run(["patches"], {"pixel_values": pv})[0]
|
| 153 |
+
enc = _V2["tk"](queries, padding=True, truncation=True, max_length=512, return_tensors="np")
|
| 154 |
+
lab = _V2["txt"].run(["embeds"], {"input_ids": enc["input_ids"].astype(np.int64),
|
| 155 |
+
"attention_mask": enc["attention_mask"].astype(np.int64)})[0]
|
| 156 |
+
probs = _V2["head"].run(["probs"], {"patches": patches.astype(np.float32),
|
| 157 |
+
"label_embeds": lab.astype(np.float32)})[0][0].tolist()
|
| 158 |
+
return probs, None
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
# ---------- v1 open-weight: public ONNX (image + text towers) ----------
|
| 162 |
+
def _ensure_v1():
|
| 163 |
+
if _V1:
|
| 164 |
+
return
|
| 165 |
+
try:
|
| 166 |
+
img = hf_hub_download(V1_REPO, "modules/omni-image/image_model.onnx", repo_type="model", token=TOKEN)
|
| 167 |
+
txt = hf_hub_download(V1_REPO, "modules/omni-image/text_model.onnx", repo_type="model", token=TOKEN)
|
| 168 |
+
import json
|
| 169 |
+
cfg = hf_hub_download(V1_REPO, "modules/omni-image/config.json", repo_type="model", token=TOKEN)
|
| 170 |
+
cal = json.load(open(cfg)).get("calibration") or {"scale": 1.0, "bias": 0.0}
|
| 171 |
+
_V1["img"] = _ort(img); _V1["txt"] = _ort(txt)
|
| 172 |
+
_V1["tok"] = AutoTokenizer.from_pretrained(V1_REPO, subfolder="modules/omni-image", token=TOKEN)
|
| 173 |
+
_V1["proc"] = AutoImageProcessor.from_pretrained(V1_REPO, subfolder="modules/omni-image", token=TOKEN)
|
| 174 |
+
_V1["scale"], _V1["bias"] = float(cal["scale"]), float(cal["bias"])
|
| 175 |
+
except Exception as e: # noqa: BLE001
|
| 176 |
+
_V1["error"] = f"{type(e).__name__}"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def _run_v1(image, queries):
|
| 180 |
+
_ensure_v1()
|
| 181 |
+
if "error" in _V1:
|
| 182 |
+
return None, f"⚠️ v1 unavailable ({_V1['error']})."
|
| 183 |
+
pix = _V1["proc"](images=[image.convert("RGB")], return_tensors="np")["pixel_values"].astype(np.float16)
|
| 184 |
+
ie = _V1["img"].run(["image_emb"], {"pixel_values": pix})[0] # [1,D]
|
| 185 |
+
enc = _V1["tok"](queries, padding=True, truncation=True, max_length=64, return_tensors="np")
|
| 186 |
+
te = _V1["txt"].run(["text_emb"], {"input_ids": enc["input_ids"].astype(np.int64),
|
| 187 |
+
"attention_mask": enc["attention_mask"].astype(np.int64)})[0] # [N,D]
|
| 188 |
+
cos = (ie @ te.T)[0]
|
| 189 |
+
probs = (1.0 / (1.0 + np.exp(-(_V1["scale"] * cos + _V1["bias"])))).tolist()
|
| 190 |
+
return probs, None
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
LINKS = ("→ [model](https://huggingface.co/nutrientdocs/document-classification-v2) · "
|
| 194 |
+
"[leaderboard](https://huggingface.co/spaces/nutrientdocs/document-classification-leaderboard) · "
|
| 195 |
+
"[benchmark](https://huggingface.co/datasets/nutrientdocs/document-classification-benchmark)")
|
| 196 |
+
|
| 197 |
+
MODELS = {"v2 flagship — best accuracy (commercial)": "v2",
|
| 198 |
+
"v1 — open-weight (downloadable)": "v1"}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
@spaces.GPU(duration=120)
|
| 202 |
+
def classify(image, text, model_name):
|
| 203 |
+
if image is None:
|
| 204 |
+
return "Upload a document image first."
|
| 205 |
+
labels, queries = _parse(text)
|
| 206 |
+
if not labels:
|
| 207 |
+
return "Add at least one class (one <b>label</b> per line)."
|
| 208 |
+
probs, err = (_run_v2 if MODELS.get(model_name) == "v2" else _run_v1)(image, queries)
|
| 209 |
+
return err if err else _table(labels, probs)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
with gr.Blocks(title="Nutrient Document Classification", theme=gr.themes.Soft()) as demo:
|
| 213 |
+
gr.Markdown("# Classify any document against labels you choose\n"
|
| 214 |
+
"Open-vocabulary, zero-shot — add candidate classes (label + optional description), upload a "
|
| 215 |
+
"page, and the model ranks them. No fixed class list.\n\n" + LINKS)
|
| 216 |
+
with gr.Row():
|
| 217 |
+
img = gr.Image(type="pil", label="Document image", height=360)
|
| 218 |
+
with gr.Column():
|
| 219 |
+
model = gr.Radio(list(MODELS), value=list(MODELS)[0], label="Model")
|
| 220 |
+
tbl = gr.Textbox(value=DEFAULT_TEXT, lines=10, label="Candidate classes",
|
| 221 |
+
info="One class per line — `label | description` (the description is optional).")
|
| 222 |
+
btn = gr.Button("Classify", variant="primary")
|
| 223 |
+
out = gr.HTML()
|
| 224 |
+
sel = gr.Textbox(visible=False)
|
| 225 |
+
gr.Examples(examples=[[n] for n in EXAMPLE_NAMES], inputs=[sel], outputs=[img, tbl],
|
| 226 |
+
fn=_load_example, run_on_click=True, cache_examples=False,
|
| 227 |
+
label="Examples the model classifies correctly — click to load the page + its labels")
|
| 228 |
+
btn.click(classify, [img, tbl, model], out)
|
| 229 |
+
|
| 230 |
+
def _q(request: gr.Request):
|
| 231 |
+
m = (request.query_params or {}).get("model", "")
|
| 232 |
+
return list(MODELS)[1] if "v1" in m.lower() else list(MODELS)[0]
|
| 233 |
+
demo.load(_q, None, model)
|
| 234 |
+
|
| 235 |
+
demo.queue().launch(ssr_mode=False)
|
examples/bar-chart_1.png
ADDED
|
examples/financial-reports_1.png
ADDED
|
examples/financial-reports_2.png
ADDED
|
Git LFS Details
|
examples/manuals_1.png
ADDED
|
Git LFS Details
|
examples/manuals_2.png
ADDED
|
examples/pie-chart_1.png
ADDED
|
examples/scientific-articles_1.png
ADDED
|
examples/scientific-articles_2.png
ADDED
|
Git LFS Details
|
examples/screenshot_1.png
ADDED
|
examples/signature_1.png
ADDED
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
transformers>=4.49
|
| 4 |
+
onnxruntime
|
| 5 |
+
sentencepiece
|
| 6 |
+
protobuf
|
| 7 |
+
pillow
|
| 8 |
+
accelerate
|
| 9 |
+
spaces
|