airayven7 commited on
Commit
deb5a88
·
1 Parent(s): c63fd20

adding pdf parse capability

Browse files
Files changed (3) hide show
  1. README.md +4 -0
  2. app.py +163 -8
  3. requirements.txt +12 -0
README.md CHANGED
@@ -8,6 +8,10 @@ sdk_version: 6.16.0
8
  python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
 
 
11
  license: mit
12
  ---
13
 
 
8
  python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
+ short_description: Parse repair-manual PDF pages with NVIDIA Nemotron Parse v1.2
12
+ preload_from_hub:
13
+ - nvidia/NVIDIA-Nemotron-Parse-v1.2
14
+ - nvidia/C-RADIOv2-H
15
  license: mit
16
  ---
17
 
app.py CHANGED
@@ -1,14 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import spaces
3
  import torch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- zero = torch.Tensor([0]).cuda()
6
- print(zero.device) # <-- 'cpu' 🤔
 
 
 
7
 
8
- @spaces.GPU
9
- def greet(n):
10
- print(zero.device) # <-- 'cuda:0' 🤗
11
- return f"Hello {zero + n} Tensor"
12
 
13
- demo = gr.Interface(fn=greet, inputs=gr.Number(), outputs=gr.Text())
14
- demo.launch()
 
1
+ """Gradio + ZeroGPU Space for NVIDIA Nemotron Parse v1.2.
2
+
3
+ Upload a PDF, pick a page, and get back the parsed markdown, a structured JSON of
4
+ elements, and the page image annotated with bounding boxes.
5
+
6
+ Runs on ZeroGPU: the model is loaded onto cuda at module level (ZeroGPU emulates
7
+ CUDA at startup) and inference runs inside an @spaces.GPU-decorated function.
8
+
9
+ This file targets the Space (cuda/bfloat16). For local CPU testing use
10
+ parse_page.py in the repo root instead.
11
+ """
12
+
13
+ import json
14
+ import sys
15
+
16
+ import fitz # pymupdf
17
  import gradio as gr
18
  import spaces
19
  import torch
20
+ from huggingface_hub import snapshot_download
21
+ from PIL import Image, ImageDraw
22
+ from transformers import AutoModel, AutoProcessor, GenerationConfig
23
+
24
+ MODEL_ID = "nvidia/NVIDIA-Nemotron-Parse-v1.2"
25
+ DEVICE = "cuda"
26
+ DTYPE = torch.bfloat16
27
+ MAX_PROMPT_DURATION = 120 # seconds of GPU time per page
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Load helpers + model once at module level (ZeroGPU loads cuda weights here).
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def load_postprocessing():
35
+ """Download the repo's .py helpers and import postprocessing.
36
+
37
+ postprocessing.py imports sibling modules (latex2html, ...), so we pull all
38
+ top-level .py files into one dir and put it on sys.path before importing.
39
+ """
40
+ repo_dir = snapshot_download(repo_id=MODEL_ID, allow_patterns=["*.py"])
41
+ if repo_dir not in sys.path:
42
+ sys.path.insert(0, repo_dir)
43
+ import postprocessing # noqa: E402 (resolved via sys.path above)
44
+
45
+ return postprocessing
46
+
47
+
48
+ pp = load_postprocessing()
49
+
50
+ # Every load passes trust_remote_code=True so the nested C-RADIO encoder code is
51
+ # accepted non-interactively (no [y/N] prompt to hang the Space build).
52
+ model = (
53
+ AutoModel.from_pretrained(MODEL_ID, trust_remote_code=True, dtype=DTYPE)
54
+ .to(DEVICE)
55
+ .eval()
56
+ )
57
+ processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
58
+ generation_config = GenerationConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
59
+
60
+
61
+ @spaces.GPU(duration=MAX_PROMPT_DURATION)
62
+ def run_model(image: Image.Image, task_prompt: str) -> str:
63
+ """GPU-only step: preprocess + generate + decode. Returns raw model text."""
64
+ inputs = processor(
65
+ images=[image], text=task_prompt, return_tensors="pt", add_special_tokens=False
66
+ )
67
+ # Move to GPU; cast float tensors (pixel_values) to the model dtype.
68
+ inputs = {
69
+ k: (v.to(DEVICE, DTYPE) if torch.is_floating_point(v) else v.to(DEVICE))
70
+ for k, v in inputs.items()
71
+ }
72
+ with torch.no_grad():
73
+ outputs = model.generate(**inputs, generation_config=generation_config)
74
+ return processor.batch_decode(outputs, skip_special_tokens=True)[0]
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # CPU-side orchestration: render page, call GPU, postprocess, annotate.
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ def render_page(pdf_path: str, page_num: int, dpi: int) -> Image.Image:
83
+ doc = fitz.open(pdf_path)
84
+ try:
85
+ if page_num < 1 or page_num > doc.page_count:
86
+ raise gr.Error(
87
+ f"Page {page_num} out of range — this PDF has {doc.page_count} pages."
88
+ )
89
+ pix = doc.load_page(page_num - 1).get_pixmap(dpi=dpi)
90
+ return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
91
+ finally:
92
+ doc.close()
93
+
94
+
95
+ def parse(pdf_file, page_num, dpi, text_in_pic, table_format):
96
+ if pdf_file is None:
97
+ raise gr.Error("Please upload a PDF first.")
98
+
99
+ image = render_page(pdf_file, int(page_num), int(dpi))
100
+
101
+ fourth = "<predict_text_in_pic>" if text_in_pic else "<predict_no_text_in_pic>"
102
+ task_prompt = f"</s><s><predict_bbox><predict_classes><output_markdown>{fourth}"
103
+
104
+ generated_text = run_model(image, task_prompt)
105
+
106
+ classes, bboxes, texts = pp.extract_classes_bboxes(generated_text)
107
+ bboxes = [pp.transform_bbox_to_original(b, image.width, image.height) for b in bboxes]
108
+ texts = [
109
+ pp.postprocess_text(t, cls=c, table_format=table_format, text_format="markdown")
110
+ for t, c in zip(texts, classes)
111
+ ]
112
+
113
+ markdown = "\n\n".join(texts)
114
+ elements = [
115
+ {"class": c, "bbox": b, "text": t} for c, b, t in zip(classes, bboxes, texts)
116
+ ]
117
+
118
+ annotated = image.copy()
119
+ draw = ImageDraw.Draw(annotated)
120
+ for b in bboxes:
121
+ draw.rectangle((b[0], b[1], b[2], b[3]), outline="red", width=2)
122
+
123
+ return annotated, markdown, json.dumps(elements, indent=2)
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # UI
128
+ # ---------------------------------------------------------------------------
129
+
130
+ with gr.Blocks(title="Nemotron Parse — Repair Manuals") as demo:
131
+ gr.Markdown(
132
+ "# 🔧 Nemotron Parse v1.2 — Repair Manual Explorer\n"
133
+ "Upload a PDF, choose a page, and parse it with "
134
+ "[NVIDIA Nemotron Parse v1.2](https://huggingface.co/nvidia/NVIDIA-Nemotron-Parse-v1.2) "
135
+ "on ZeroGPU. Returns structured markdown, a JSON of elements, and an "
136
+ "annotated page image."
137
+ )
138
+ with gr.Row():
139
+ with gr.Column(scale=1):
140
+ pdf_in = gr.File(label="PDF", file_types=[".pdf"], type="filepath")
141
+ page_in = gr.Number(label="Page", value=1, precision=0, minimum=1)
142
+ dpi_in = gr.Slider(
143
+ label="Render DPI", minimum=72, maximum=300, value=150, step=10
144
+ )
145
+ text_in_pic_in = gr.Checkbox(
146
+ label="Extract text inside pictures/diagrams", value=False
147
+ )
148
+ table_format_in = gr.Dropdown(
149
+ label="Table format",
150
+ choices=["markdown", "latex", "HTML", "json", "csv"],
151
+ value="markdown",
152
+ )
153
+ run_btn = gr.Button("Parse page", variant="primary")
154
+ with gr.Column(scale=2):
155
+ img_out = gr.Image(label="Annotated page", type="pil")
156
+ with gr.Tab("Rendered markdown"):
157
+ md_out = gr.Markdown()
158
+ with gr.Tab("Structured JSON"):
159
+ json_out = gr.Code(language="json")
160
 
161
+ run_btn.click(
162
+ parse,
163
+ inputs=[pdf_in, page_in, dpi_in, text_in_pic_in, table_format_in],
164
+ outputs=[img_out, md_out, json_out],
165
+ )
166
 
 
 
 
 
167
 
168
+ if __name__ == "__main__":
169
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spaces
2
+ gradio
3
+ transformers==5.6.1
4
+ accelerate
5
+ albumentations
6
+ timm
7
+ open_clip_torch
8
+ einops
9
+ beautifulsoup4
10
+ lxml
11
+ pymupdf
12
+ pillow