Spaces:
Running on Zero
Running on Zero
electblake commited on
Commit ·
b2b415b
1
Parent(s): cee323f
Add structured receipt extraction outputs and HF launch entrypoint
Browse files- .gitignore +1 -0
- README.md +32 -0
- app.py +4 -0
- app/gradio.py +72 -12
- app/hf_space.py +7 -0
- gradio_app.py +10 -0
- mise.toml +4 -0
.gitignore
CHANGED
|
@@ -8,3 +8,4 @@ wheels/
|
|
| 8 |
|
| 9 |
# Virtual environments
|
| 10 |
.venv
|
|
|
|
|
|
| 8 |
|
| 9 |
# Virtual environments
|
| 10 |
.venv
|
| 11 |
+
**/*.log
|
README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: NuMark
|
| 3 |
+
emoji: 🧾
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: gradio
|
| 7 |
+
python_version: "3.12"
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
models:
|
| 11 |
+
- numind/NuExtract3
|
| 12 |
+
short_description: Extract Markdown or structured JSON from document images with NuExtract3.
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# NuMark
|
| 16 |
+
|
| 17 |
+
NuMark is a Gradio app for extracting Markdown or structured JSON from document images with [NuExtract3](https://huggingface.co/numind/NuExtract3).
|
| 18 |
+
|
| 19 |
+
Upload a receipt, invoice, or other document image, optionally add accompanying text, and choose either content extraction or schema-guided structured extraction.
|
| 20 |
+
|
| 21 |
+
The inference function uses Hugging Face ZeroGPU dynamic GPU allocation. Select ZeroGPU in the Space hardware settings before launching the app.
|
| 22 |
+
|
| 23 |
+
## Run locally
|
| 24 |
+
|
| 25 |
+
Install the project dependencies and launch the Hugging Face Spaces entry point:
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
uv sync
|
| 29 |
+
uv run app.py
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
The Hugging Face Space starts from `app.py`, which exposes the Gradio app defined in `app/hf_space.py`.
|
app.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.hf_space import demo
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
demo.launch()
|
app/gradio.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
import gradio as gr
|
|
|
|
| 4 |
import torch
|
| 5 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 6 |
|
|
@@ -17,6 +19,40 @@ structured_json_templates = {
|
|
| 17 |
"paid": "boolean",
|
| 18 |
"payment_method": ["cash", "credit-card", "debit-card", "other"],
|
| 19 |
"items": ["verbatim-string"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}""",
|
| 21 |
"Invoice with line items": """{
|
| 22 |
"document_type": "verbatim-string",
|
|
@@ -99,19 +135,24 @@ default_structured_json_template = "Invoice with line items"
|
|
| 99 |
def select_structured_json_template(name):
|
| 100 |
return structured_json_templates[name]
|
| 101 |
|
|
|
|
| 102 |
processor = AutoProcessor.from_pretrained(
|
| 103 |
model_id,
|
| 104 |
trust_remote_code=True,
|
| 105 |
)
|
| 106 |
|
| 107 |
-
model =
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
)
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
|
|
|
|
| 115 |
def run_nuextract(messages, **chat_template_kwargs):
|
| 116 |
inputs = processor.apply_chat_template(
|
| 117 |
messages,
|
|
@@ -138,7 +179,7 @@ def run_nuextract(messages, **chat_template_kwargs):
|
|
| 138 |
|
| 139 |
|
| 140 |
def extract(image, text, mode, template, enable_thinking):
|
| 141 |
-
|
| 142 |
[
|
| 143 |
{
|
| 144 |
"role": "user",
|
|
@@ -153,6 +194,11 @@ def extract(image, text, mode, template, enable_thinking):
|
|
| 153 |
enable_thinking=enable_thinking,
|
| 154 |
)
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
with gr.Blocks(title="NuExtract3") as demo:
|
| 158 |
gr.Markdown(
|
|
@@ -186,7 +232,21 @@ with gr.Blocks(title="NuExtract3") as demo:
|
|
| 186 |
)
|
| 187 |
enable_thinking = gr.Checkbox(label="Enable thinking")
|
| 188 |
run = gr.Button("Extract", variant="primary")
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
gr.Examples(
|
| 192 |
examples=[
|
|
@@ -208,9 +268,9 @@ with gr.Blocks(title="NuExtract3") as demo:
|
|
| 208 |
/ "invoice-with-items.png",
|
| 209 |
*sorted(
|
| 210 |
path
|
| 211 |
-
for path in (
|
| 212 |
-
|
| 213 |
-
)
|
| 214 |
if path.name != "invoice-with-items.png"
|
| 215 |
),
|
| 216 |
]
|
|
@@ -227,7 +287,7 @@ with gr.Blocks(title="NuExtract3") as demo:
|
|
| 227 |
run.click(
|
| 228 |
extract,
|
| 229 |
inputs=[image, text, mode, template, enable_thinking],
|
| 230 |
-
outputs=
|
| 231 |
api_name="extract",
|
| 232 |
)
|
| 233 |
|
|
|
|
| 1 |
+
import json
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
import gradio as gr
|
| 5 |
+
import spaces
|
| 6 |
import torch
|
| 7 |
from transformers import AutoModelForImageTextToText, AutoProcessor
|
| 8 |
|
|
|
|
| 19 |
"paid": "boolean",
|
| 20 |
"payment_method": ["cash", "credit-card", "debit-card", "other"],
|
| 21 |
"items": ["verbatim-string"]
|
| 22 |
+
}""",
|
| 23 |
+
"Advanced receipt": """{
|
| 24 |
+
"merchant": {
|
| 25 |
+
"name": "verbatim-string",
|
| 26 |
+
"address": "verbatim-string",
|
| 27 |
+
"phone": "phone-number"
|
| 28 |
+
},
|
| 29 |
+
"purchase_date": "date",
|
| 30 |
+
"purchase_time": "time",
|
| 31 |
+
"store_number": "verbatim-string",
|
| 32 |
+
"operator_number": "verbatim-string",
|
| 33 |
+
"terminal_number": "verbatim-string",
|
| 34 |
+
"transaction_number": "verbatim-string",
|
| 35 |
+
"items": [
|
| 36 |
+
{
|
| 37 |
+
"description": "verbatim-string",
|
| 38 |
+
"sku": "verbatim-string",
|
| 39 |
+
"quantity": "number",
|
| 40 |
+
"unit": "unit-code",
|
| 41 |
+
"unit_price": "number",
|
| 42 |
+
"discount": "number",
|
| 43 |
+
"line_total": "number",
|
| 44 |
+
"tax_code": "verbatim-string"
|
| 45 |
+
}
|
| 46 |
+
],
|
| 47 |
+
"item_count": "integer",
|
| 48 |
+
"subtotal": "number",
|
| 49 |
+
"tax": "number",
|
| 50 |
+
"total": "number",
|
| 51 |
+
"currency": "currency",
|
| 52 |
+
"payment_method": ["cash", "credit-card", "debit-card", "other"],
|
| 53 |
+
"card_last_four": "verbatim-string",
|
| 54 |
+
"amount_tendered": "number",
|
| 55 |
+
"change_due": "number"
|
| 56 |
}""",
|
| 57 |
"Invoice with line items": """{
|
| 58 |
"document_type": "verbatim-string",
|
|
|
|
| 135 |
def select_structured_json_template(name):
|
| 136 |
return structured_json_templates[name]
|
| 137 |
|
| 138 |
+
|
| 139 |
processor = AutoProcessor.from_pretrained(
|
| 140 |
model_id,
|
| 141 |
trust_remote_code=True,
|
| 142 |
)
|
| 143 |
|
| 144 |
+
model = (
|
| 145 |
+
AutoModelForImageTextToText.from_pretrained(
|
| 146 |
+
model_id,
|
| 147 |
+
dtype=torch.bfloat16,
|
| 148 |
+
trust_remote_code=True,
|
| 149 |
+
)
|
| 150 |
+
.to("cuda")
|
| 151 |
+
.eval()
|
| 152 |
+
)
|
| 153 |
|
| 154 |
|
| 155 |
+
@spaces.GPU(duration=120)
|
| 156 |
def run_nuextract(messages, **chat_template_kwargs):
|
| 157 |
inputs = processor.apply_chat_template(
|
| 158 |
messages,
|
|
|
|
| 179 |
|
| 180 |
|
| 181 |
def extract(image, text, mode, template, enable_thinking):
|
| 182 |
+
result = run_nuextract(
|
| 183 |
[
|
| 184 |
{
|
| 185 |
"role": "user",
|
|
|
|
| 194 |
enable_thinking=enable_thinking,
|
| 195 |
)
|
| 196 |
|
| 197 |
+
if mode == "content":
|
| 198 |
+
return result, result, gr.skip()
|
| 199 |
+
|
| 200 |
+
return gr.skip(), gr.skip(), json.loads(result)
|
| 201 |
+
|
| 202 |
|
| 203 |
with gr.Blocks(title="NuExtract3") as demo:
|
| 204 |
gr.Markdown(
|
|
|
|
| 232 |
)
|
| 233 |
enable_thinking = gr.Checkbox(label="Enable thinking")
|
| 234 |
run = gr.Button("Extract", variant="primary")
|
| 235 |
+
with gr.Column(), gr.Tabs():
|
| 236 |
+
with gr.Tab("Markdown preview"):
|
| 237 |
+
markdown_preview = gr.Markdown()
|
| 238 |
+
with gr.Tab("Markdown text"):
|
| 239 |
+
markdown_text = gr.Textbox(
|
| 240 |
+
label="Markdown source",
|
| 241 |
+
lines=28,
|
| 242 |
+
buttons=["copy"],
|
| 243 |
+
)
|
| 244 |
+
with gr.Tab("Structured output"):
|
| 245 |
+
structured_output = gr.JSON(
|
| 246 |
+
label="Structured JSON",
|
| 247 |
+
open=True,
|
| 248 |
+
show_indices=True,
|
| 249 |
+
)
|
| 250 |
|
| 251 |
gr.Examples(
|
| 252 |
examples=[
|
|
|
|
| 268 |
/ "invoice-with-items.png",
|
| 269 |
*sorted(
|
| 270 |
path
|
| 271 |
+
for path in (Path(__file__).parents[1] / "data" / "samples").glob(
|
| 272 |
+
"*.png"
|
| 273 |
+
)
|
| 274 |
if path.name != "invoice-with-items.png"
|
| 275 |
),
|
| 276 |
]
|
|
|
|
| 287 |
run.click(
|
| 288 |
extract,
|
| 289 |
inputs=[image, text, mode, template, enable_thinking],
|
| 290 |
+
outputs=[markdown_preview, markdown_text, structured_output],
|
| 291 |
api_name="extract",
|
| 292 |
)
|
| 293 |
|
app/hf_space.py
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.gradio import demo
|
| 2 |
+
|
| 3 |
+
__all__ = ["demo"]
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
if __name__ == "__main__":
|
| 7 |
+
demo.launch()
|
gradio_app.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from runpy import run_path
|
| 2 |
+
|
| 3 |
+
app = run_path("app/gradio.py")
|
| 4 |
+
demo = app["demo"]
|
| 5 |
+
|
| 6 |
+
__all__ = ["demo"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
demo.launch()
|
mise.toml
CHANGED
|
@@ -6,3 +6,7 @@ uv = "latest"
|
|
| 6 |
[env]
|
| 7 |
UV_LINK_MODE="copy"
|
| 8 |
PYTHONPATH=""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
[env]
|
| 7 |
UV_LINK_MODE="copy"
|
| 8 |
PYTHONPATH=""
|
| 9 |
+
|
| 10 |
+
[tasks.gradio]
|
| 11 |
+
description = "Launch the Gradio app with hot reload"
|
| 12 |
+
run = '.venv\Scripts\gradio.exe gradio_app.py --watch-dirs app'
|