electblake commited on
Commit
b2b415b
·
1 Parent(s): cee323f

Add structured receipt extraction outputs and HF launch entrypoint

Browse files
Files changed (7) hide show
  1. .gitignore +1 -0
  2. README.md +32 -0
  3. app.py +4 -0
  4. app/gradio.py +72 -12
  5. app/hf_space.py +7 -0
  6. gradio_app.py +10 -0
  7. 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 = AutoModelForImageTextToText.from_pretrained(
108
- model_id,
109
- dtype=torch.bfloat16,
110
- device_map="auto",
111
- trust_remote_code=True,
112
- ).eval()
 
 
 
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
- return run_nuextract(
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
- output = gr.Textbox(label="Output", lines=28, buttons=["copy"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Path(__file__).parents[1] / "data" / "samples"
213
- ).glob("*.png")
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=output,
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'