electblake commited on
Commit
72b6c80
·
1 Parent(s): c2a765a

fix(app): complete two-module consolidation

Browse files

Add the ZeroGPU model module, remove every remaining legacy launcher and package file, and update the runtime documentation after the source move.

Files changed (9) hide show
  1. README.md +1 -1
  2. app.py +2 -101
  3. app/__init__.py +0 -0
  4. app/__main__.py +0 -0
  5. app/hf_space.py +0 -7
  6. app/nuextract.py +0 -68
  7. gradio_app.py +0 -10
  8. launch.py +0 -28
  9. model.py +108 -0
README.md CHANGED
@@ -30,4 +30,4 @@ uv sync
30
  uv run app.py
31
  ```
32
 
33
- The Hugging Face Space starts from `app.py`, which exposes the Gradio app defined in `app/hf_space.py`.
 
30
  uv run app.py
31
  ```
32
 
33
+ The Hugging Face Space starts from `app.py`. Model loading and the ZeroGPU pipelines are defined in `model.py`.
app.py CHANGED
@@ -1,15 +1,8 @@
1
- # ruff: noqa: I001
2
-
3
- import spaces
4
-
5
- import json
6
  from pathlib import Path
7
 
8
  import gradio as gr
9
- import torch
10
- from transformers import AutoModelForImageTextToText, AutoProcessor
11
 
12
- model_id = "numind/NuExtract3"
13
 
14
  structured_json_templates = {
15
  "Basic receipt": """{
@@ -215,98 +208,6 @@ structured_json_templates = {
215
  default_structured_json_template = "Invoice with line items"
216
 
217
 
218
- def select_structured_json_template(name):
219
- return structured_json_templates[name]
220
-
221
-
222
- processor = AutoProcessor.from_pretrained(
223
- model_id,
224
- trust_remote_code=True,
225
- )
226
-
227
- model = (
228
- AutoModelForImageTextToText.from_pretrained(
229
- model_id,
230
- attn_implementation="sdpa",
231
- dtype=torch.bfloat16,
232
- trust_remote_code=True,
233
- )
234
- .to("cuda")
235
- .eval()
236
- )
237
-
238
-
239
- def run_nuextract(messages, **chat_template_kwargs):
240
- inputs = processor.apply_chat_template(
241
- messages,
242
- add_generation_prompt=True,
243
- tokenize=True,
244
- return_dict=True,
245
- return_tensors="pt",
246
- **chat_template_kwargs,
247
- ).to(model.device)
248
-
249
- with torch.inference_mode():
250
- generated_ids = model.generate( # pyright: ignore[reportAttributeAccessIssue]
251
- **inputs,
252
- max_new_tokens=4096,
253
- do_sample=False,
254
- )
255
-
256
- generated_ids = generated_ids[:, inputs.input_ids.shape[1] :]
257
- return processor.batch_decode(
258
- generated_ids,
259
- skip_special_tokens=True,
260
- clean_up_tokenization_spaces=False,
261
- )[0].strip()
262
-
263
-
264
- @spaces.GPU
265
- def extract(image, text, template, enable_thinking):
266
- result = run_nuextract(
267
- [
268
- {
269
- "role": "user",
270
- "content": [
271
- {"type": "image", "image": image},
272
- {"type": "text", "text": text},
273
- ],
274
- }
275
- ],
276
- mode="structured",
277
- template=template,
278
- enable_thinking=enable_thinking,
279
- )
280
-
281
- return json.loads(result)
282
-
283
-
284
- @spaces.GPU
285
- def generate_template(image):
286
- result = run_nuextract(
287
- [
288
- {
289
- "role": "user",
290
- "content": [
291
- {"type": "image", "image": image},
292
- {
293
- "type": "text",
294
- "text": (
295
- "Create a reusable structured extraction template grounded only "
296
- "in the visible document. Include fields supported by the document, "
297
- "represent repeated records as arrays, use NuExtract template leaf "
298
- "types, and return only the JSON template."
299
- ),
300
- },
301
- ],
302
- }
303
- ],
304
- mode="template-generation",
305
- )
306
-
307
- return json.dumps(json.loads(result), indent=2)
308
-
309
-
310
  with gr.Blocks(title="NuMarkApp") as demo:
311
  gr.Markdown(
312
  "# NuMarkApp\n"
@@ -411,7 +312,7 @@ with gr.Blocks(title="NuMarkApp") as demo:
411
  )
412
 
413
  template_preset.change(
414
- select_structured_json_template,
415
  inputs=template_preset,
416
  outputs=template,
417
  )
 
 
 
 
 
 
1
  from pathlib import Path
2
 
3
  import gradio as gr
 
 
4
 
5
+ from model import extract, generate_template
6
 
7
  structured_json_templates = {
8
  "Basic receipt": """{
 
208
  default_structured_json_template = "Invoice with line items"
209
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  with gr.Blocks(title="NuMarkApp") as demo:
212
  gr.Markdown(
213
  "# NuMarkApp\n"
 
312
  )
313
 
314
  template_preset.change(
315
+ structured_json_templates.__getitem__,
316
  inputs=template_preset,
317
  outputs=template,
318
  )
app/__init__.py DELETED
File without changes
app/__main__.py DELETED
File without changes
app/hf_space.py DELETED
@@ -1,7 +0,0 @@
1
- from app.gradio import demo
2
-
3
- __all__ = ["demo"]
4
-
5
-
6
- if __name__ == "__main__":
7
- demo.launch()
 
 
 
 
 
 
 
 
app/nuextract.py DELETED
@@ -1,68 +0,0 @@
1
- from pathlib import Path
2
-
3
- import torch
4
- from PIL import Image
5
- from transformers import AutoModelForImageTextToText, AutoProcessor
6
-
7
- model_id = "numind/NuExtract3"
8
-
9
- processor = AutoProcessor.from_pretrained(
10
- model_id,
11
- trust_remote_code=True,
12
- )
13
-
14
- model = AutoModelForImageTextToText.from_pretrained(
15
- model_id,
16
- dtype=torch.bfloat16,
17
- device_map="auto",
18
- trust_remote_code=True,
19
- ).eval()
20
-
21
-
22
- def run_nuextract(messages, **chat_template_kwargs):
23
- inputs = processor.apply_chat_template(
24
- messages,
25
- add_generation_prompt=True,
26
- tokenize=True,
27
- return_dict=True,
28
- return_tensors="pt",
29
- **chat_template_kwargs,
30
- ).to(model.device)
31
-
32
- with torch.inference_mode():
33
- generated_ids = model.generate( # pyright: ignore[reportAttributeAccessIssue]
34
- **inputs,
35
- max_new_tokens=4096,
36
- do_sample=False,
37
- )
38
-
39
- generated_ids = generated_ids[:, inputs.input_ids.shape[1] :]
40
- return processor.batch_decode(
41
- generated_ids,
42
- skip_special_tokens=True,
43
- clean_up_tokenization_spaces=False,
44
- )[0].strip()
45
-
46
-
47
- sample_path = (
48
- Path(__file__).parents[1] / "data" / "samples" / "Screenshot 2026-07-18 171416.png"
49
- )
50
- sample_image = Image.open(sample_path).convert("RGB")
51
- messages = [
52
- {
53
- "role": "user",
54
- "content": [
55
- {
56
- "type": "image",
57
- "image": sample_image,
58
- }
59
- ],
60
- }
61
- ]
62
-
63
- output = run_nuextract(
64
- messages,
65
- mode="content",
66
- enable_thinking=False,
67
- )
68
- print(output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gradio_app.py DELETED
@@ -1,10 +0,0 @@
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()
 
 
 
 
 
 
 
 
 
 
 
launch.py DELETED
@@ -1,28 +0,0 @@
1
- import argparse
2
- import runpy
3
-
4
-
5
- MODULES = {
6
- "gradio": "app.gradio",
7
- "hf_space": "app.hf_space",
8
- "nuextract": "app.nuextract",
9
- }
10
-
11
-
12
- def build_parser() -> argparse.ArgumentParser:
13
- parser = argparse.ArgumentParser(description="Launch a NuMark application.")
14
- parser.add_argument(
15
- "app",
16
- choices=MODULES,
17
- help="Application to launch.",
18
- )
19
- return parser
20
-
21
-
22
- def main() -> None:
23
- args = build_parser().parse_args()
24
- runpy.run_module(MODULES[args.app], run_name="__main__")
25
-
26
-
27
- if __name__ == "__main__":
28
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
model.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ruff: noqa: I001
2
+
3
+ import spaces
4
+
5
+ import json
6
+
7
+ import torch
8
+ from transformers import AutoModelForImageTextToText, AutoProcessor
9
+
10
+
11
+ processor = AutoProcessor.from_pretrained(
12
+ "numind/NuExtract3",
13
+ trust_remote_code=True,
14
+ )
15
+
16
+ model = (
17
+ AutoModelForImageTextToText.from_pretrained(
18
+ "numind/NuExtract3",
19
+ attn_implementation="sdpa",
20
+ dtype=torch.bfloat16,
21
+ trust_remote_code=True,
22
+ )
23
+ .to("cuda")
24
+ .eval()
25
+ )
26
+
27
+
28
+ @spaces.GPU
29
+ def extract(image, text, template, enable_thinking):
30
+ inputs = processor.apply_chat_template(
31
+ [
32
+ {
33
+ "role": "user",
34
+ "content": [
35
+ {"type": "image", "image": image},
36
+ {"type": "text", "text": text},
37
+ ],
38
+ }
39
+ ],
40
+ add_generation_prompt=True,
41
+ tokenize=True,
42
+ return_dict=True,
43
+ return_tensors="pt",
44
+ mode="structured",
45
+ template=template,
46
+ enable_thinking=enable_thinking,
47
+ ).to(model.device)
48
+
49
+ with torch.inference_mode():
50
+ generated_ids = model.generate(
51
+ **inputs,
52
+ max_new_tokens=4096,
53
+ do_sample=False,
54
+ )
55
+
56
+ return json.loads(
57
+ processor.batch_decode(
58
+ generated_ids[:, inputs.input_ids.shape[1] :],
59
+ skip_special_tokens=True,
60
+ clean_up_tokenization_spaces=False,
61
+ )[0].strip()
62
+ )
63
+
64
+
65
+ @spaces.GPU
66
+ def generate_template(image):
67
+ inputs = processor.apply_chat_template(
68
+ [
69
+ {
70
+ "role": "user",
71
+ "content": [
72
+ {"type": "image", "image": image},
73
+ {
74
+ "type": "text",
75
+ "text": (
76
+ "Create a reusable structured extraction template grounded only "
77
+ "in the visible document. Include fields supported by the document, "
78
+ "represent repeated records as arrays, use NuExtract template leaf "
79
+ "types, and return only the JSON template."
80
+ ),
81
+ },
82
+ ],
83
+ }
84
+ ],
85
+ add_generation_prompt=True,
86
+ tokenize=True,
87
+ return_dict=True,
88
+ return_tensors="pt",
89
+ mode="template-generation",
90
+ ).to(model.device)
91
+
92
+ with torch.inference_mode():
93
+ generated_ids = model.generate(
94
+ **inputs,
95
+ max_new_tokens=4096,
96
+ do_sample=False,
97
+ )
98
+
99
+ return json.dumps(
100
+ json.loads(
101
+ processor.batch_decode(
102
+ generated_ids[:, inputs.input_ids.shape[1] :],
103
+ skip_special_tokens=True,
104
+ clean_up_tokenization_spaces=False,
105
+ )[0].strip()
106
+ ),
107
+ indent=2,
108
+ )