Varun95 commited on
Commit
011af0d
·
verified ·
1 Parent(s): cfb9269

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -147
app.py CHANGED
@@ -1,25 +1,54 @@
1
  """
2
- ScriptFlow — Handwriting PDF → Word Document converter
3
- HuggingFace Spaces (CPU free tier)
4
-
5
- Pipeline:
6
- 1. PDF → page images (pdftoppm / Poppler)
7
- 2. Images → raw text (TrOCR small-handwritten)
8
- 3. Raw text → refined (T5 grammar correction, optional)
9
- 4. Refined text → .docx (python-docx)
10
  """
11
 
12
  import os
13
  import glob
14
- import subprocess
15
- import tempfile
16
  import shutil
 
 
17
  from pathlib import Path
18
 
19
  import gradio as gr
 
 
 
 
 
 
 
20
 
21
  # ──────────────────────────────────────────────────────────────────────────────
22
- # Step 1 — PDF → images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  # ──────────────────────────────────────────────────────────────────────────────
24
 
25
  def pdf_to_images(pdf_path: str, out_dir: str) -> list[str]:
@@ -27,85 +56,56 @@ def pdf_to_images(pdf_path: str, out_dir: str) -> list[str]:
27
  os.makedirs(pages_dir, exist_ok=True)
28
 
29
  result = subprocess.run(
30
- ["pdftoppm", "-png", "-r", "200", pdf_path,
31
- os.path.join(pages_dir, "page")],
32
- capture_output=True, text=True, timeout=120,
 
33
  )
34
  if result.returncode != 0:
35
  raise RuntimeError(f"pdftoppm failed: {result.stderr.strip()}")
36
 
37
  images = sorted(glob.glob(os.path.join(pages_dir, "*.png")))
38
  if not images:
39
- raise RuntimeError("No page images were generated from the PDF.")
40
  return images
41
 
42
 
43
- # ──────────────────────────────────────────────────────────────────────────────
44
- # Step 2 — Images → raw text (TrOCR)
45
- # ──────────────────────────────────────────────────────────────────────────────
46
-
47
- _trocr_cache = {}
48
-
49
- def load_trocr(model_name: str):
50
- global _trocr_cache
51
- if model_name not in _trocr_cache:
52
- from transformers import TrOCRProcessor, VisionEncoderDecoderModel
53
- processor = TrOCRProcessor.from_pretrained(model_name)
54
- model = VisionEncoderDecoderModel.from_pretrained(model_name)
55
- model.eval()
56
- _trocr_cache[model_name] = (processor, model)
57
- return _trocr_cache[model_name]
58
-
59
-
60
- def images_to_text(image_paths: list[str], model_name: str,
61
- progress: gr.Progress) -> list[str]:
62
- import torch
63
- from PIL import Image
64
-
65
- processor, model = load_trocr(model_name)
66
  page_texts = []
67
 
68
- for i, img_path in enumerate(image_paths):
69
- progress(i / len(image_paths),
70
- desc=f"OCR — page {i + 1} / {len(image_paths)}")
 
71
  image = Image.open(img_path).convert("RGB")
72
  pixel_values = processor(images=image, return_tensors="pt").pixel_values
 
73
  with torch.no_grad():
74
- ids = model.generate(pixel_values)
75
- text = processor.batch_decode(ids, skip_special_tokens=True)[0]
 
76
  page_texts.append(text.strip())
77
 
78
  return page_texts
79
 
80
 
81
- # ──────────────────────────────────────────────────────────────────────────────
82
- # Step 3 — Grammar correction (T5, optional)
83
- # ──────────────────────────────────────────────────────────────────────────────
84
-
85
- _grammar_pipe = None
86
-
87
- def load_grammar_pipeline():
88
- global _grammar_pipe
89
- if _grammar_pipe is None:
90
- from transformers import pipeline
91
- _grammar_pipe = pipeline(
92
- "text2text-generation",
93
- model="vennify/t5-base-grammar-correction",
94
- tokenizer="vennify/t5-base-grammar-correction",
95
- )
96
- return _grammar_pipe
97
-
98
 
99
- def refine_text(raw_pages: list[str], progress: gr.Progress) -> list[str]:
100
- pipe = load_grammar_pipeline()
101
- refined = []
 
 
 
 
102
 
103
- for i, page in enumerate(raw_pages):
104
- progress(i / len(raw_pages),
105
- desc=f"Grammar correction — page {i + 1} / {len(raw_pages)}")
106
- # T5 grammar model has a 512-token limit; chunk long pages
107
- chunks = [page[j:j+400] for j in range(0, len(page), 400)]
108
  corrected_chunks = []
 
109
  for chunk in chunks:
110
  if not chunk.strip():
111
  continue
@@ -116,147 +116,137 @@ def refine_text(raw_pages: list[str], progress: gr.Progress) -> list[str]:
116
  early_stopping=True,
117
  )
118
  corrected_chunks.append(out[0]["generated_text"])
119
- refined.append(" ".join(corrected_chunks) if corrected_chunks else page)
120
-
121
- return refined
122
 
 
123
 
124
- # ──────────────────────────────────────────────────────────────────────────────
125
- # Step 4 — Text → .docx
126
- # ──────────────────────────────────────────────────────────────────────────────
127
 
128
- def text_to_docx(pages: list[str], output_path: str):
129
- from docx import Document
130
- from docx.shared import Pt, Inches
131
- from docx.enum.text import WD_ALIGN_PARAGRAPH
132
 
 
133
  doc = Document()
 
134
  for section in doc.sections:
135
- section.top_margin = Inches(1)
136
  section.bottom_margin = Inches(1)
137
- section.left_margin = Inches(1.25)
138
- section.right_margin = Inches(1.25)
139
 
140
- title = doc.add_heading("Converted Handwritten Document", level=1)
141
- title.alignment = WD_ALIGN_PARAGRAPH.CENTER
142
- doc.add_paragraph("")
143
 
144
- for i, page_text in enumerate(pages):
145
- if i > 0:
146
  doc.add_page_break()
147
- for line in page_text.split("\n"):
148
- line = line.strip()
 
 
149
  p = doc.add_paragraph()
150
- if line:
151
- run = p.add_run(line)
 
152
  run.font.size = Pt(12)
153
 
154
  doc.save(output_path)
155
 
156
 
157
  # ───────────��──────────────────────────────────────────────────────────────────
158
- # Main pipeline function (called by Gradio)
159
  # ──────────────────────────────────────────────────────────────────────────────
160
 
161
- def run_pipeline(
162
- pdf_file,
163
- trocr_model: str,
164
- use_grammar: bool,
165
- progress=gr.Progress(track_tqdm=True),
166
- ):
167
  if pdf_file is None:
168
- raise gr.Error("Please upload a PDF file first.")
169
 
170
- # Handle Gradio filepath string vs object compatibility
171
- pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
172
 
173
- with tempfile.TemporaryDirectory() as tmp:
174
- # Step 1
175
- progress(0, desc="Converting PDF to images…")
176
- images = pdf_to_images(pdf_path, tmp)
177
 
178
- # Step 2
179
- progress(0.1, desc="Loading TrOCR model…")
180
- raw_pages = images_to_text(images, trocr_model, progress)
181
- raw_text = "\n\n[PAGE BREAK]\n\n".join(raw_pages)
182
 
183
- # Step 3
184
  if use_grammar:
185
- progress(0.6, desc="Loading grammar correction model…")
186
- final_pages = refine_text(raw_pages, progress)
187
  else:
188
  final_pages = raw_pages
189
 
190
- # Step 4
191
- progress(0.9, desc="Generating Word document…")
192
- out_docx = os.path.join(tmp, "output.docx")
193
- text_to_docx(final_pages, out_docx)
194
 
195
- # Copy out of the temp dir so Gradio can serve it
196
- dest = tempfile.NamedTemporaryFile(
197
- suffix=".docx", delete=False, prefix="scriptflow_"
198
- )
199
- dest.close()
200
- shutil.copy(out_docx, dest.name)
 
 
 
201
 
202
- progress(1.0, desc="Done!")
203
- final_text = "\n\n[PAGE BREAK]\n\n".join(final_pages)
204
- return raw_text, final_text, dest.name
205
 
206
 
207
  # ──────────────────────────────────────────────────────────────────────────────
208
- # Gradio UI
209
  # ──────────────────────────────────────────────────────────────────────────────
210
 
211
- with gr.Blocks(title="ScriptFlow — Handwriting to Word", theme=gr.themes.Soft()) as demo:
212
  gr.Markdown(
213
  """
214
  # ✍️ ScriptFlow
215
- ### Convert handwritten PDF pages into a clean Word (.docx) document
216
- Upload a PDF → TrOCR reads every page → (optional) T5 corrects grammar → download .docx
217
  """
218
  )
219
 
220
  with gr.Row():
221
  with gr.Column(scale=1):
222
  pdf_input = gr.File(
223
- label="Upload handwritten PDF",
224
  file_types=[".pdf"],
225
  type="filepath",
226
  )
227
- trocr_model = gr.Radio(
228
  choices=[
229
- ("Small — faster, ~330 MB", "microsoft/trocr-small-handwritten"),
230
- ("Base — more accurate, ~440 MB", "microsoft/trocr-base-handwritten"),
231
  ],
232
  value="microsoft/trocr-small-handwritten",
233
- label="TrOCR model",
234
  )
235
- use_grammar = gr.Checkbox(
236
- label="Apply T5 grammar correction (adds processing time)",
237
  value=False,
238
  )
239
- run_btn = gr.Button("Convert", variant="primary")
240
 
241
  with gr.Column(scale=2):
242
- raw_out = gr.Textbox(label="Raw OCR text", lines=12, interactive=False)
243
- final_out = gr.Textbox(label="Refined text", lines=12, interactive=False)
244
- docx_out = gr.File( label="Download Word file", interactive=False)
245
 
246
  gr.Markdown(
247
  """
248
  ---
249
- **Notes**
250
- - First run downloads models from Hugging Face — subsequent runs use the cache.
251
- - Grammar correction uses [`vennify/t5-base-grammar-correction`](https://huggingface.co/vennify/t5-base-grammar-correction).
252
- - Running on CPU: expect ~30–60 s per page for OCR.
253
  """
254
  )
255
 
256
- run_btn.click(
257
- fn=run_pipeline,
258
- inputs=[pdf_input, trocr_model, use_grammar],
259
- outputs=[raw_out, final_out, docx_out],
260
  )
261
 
262
  if __name__ == "__main__":
 
1
  """
2
+ ScriptFlow — Handwriting PDF → Word Document Converter
3
+ Hugging Face Spaces (CPU Free Tier)
 
 
 
 
 
 
4
  """
5
 
6
  import os
7
  import glob
 
 
8
  import shutil
9
+ import tempfile
10
+ import subprocess
11
  from pathlib import Path
12
 
13
  import gradio as gr
14
+ import torch
15
+ from PIL import Image
16
+ from transformers import TrOCRProcessor, VisionEncoderDecoderModel, pipeline
17
+ from docx import Document
18
+ from docx.shared import Pt, Inches
19
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
20
+
21
 
22
  # ──────────────────────────────────────────────────────────────────────────────
23
+ # Model Caching Setup
24
+ # ──────────────────────────────────────────────────────────────────────────────
25
+
26
+ _trocr_cache = {}
27
+ _grammar_pipe = None
28
+
29
+
30
+ def get_trocr_model(model_name: str):
31
+ if model_name not in _trocr_cache:
32
+ processor = TrOCRProcessor.from_pretrained(model_name)
33
+ model = VisionEncoderDecoderModel.from_pretrained(model_name)
34
+ model.eval()
35
+ _trocr_cache[model_name] = (processor, model)
36
+ return _trocr_cache[model_name]
37
+
38
+
39
+ def get_grammar_pipeline():
40
+ global _grammar_pipe
41
+ if _grammar_pipe is None:
42
+ _grammar_pipe = pipeline(
43
+ "text2text-generation",
44
+ model="vennify/t5-base-grammar-correction",
45
+ tokenizer="vennify/t5-base-grammar-correction",
46
+ )
47
+ return _grammar_pipe
48
+
49
+
50
+ # ──────────────────────────────────────────────────────────────────────────────
51
+ # Core Processing Functions
52
  # ──────────────────────────────────────────────────────────────────────────────
53
 
54
  def pdf_to_images(pdf_path: str, out_dir: str) -> list[str]:
 
56
  os.makedirs(pages_dir, exist_ok=True)
57
 
58
  result = subprocess.run(
59
+ ["pdftoppm", "-png", "-r", "200", pdf_path, os.path.join(pages_dir, "page")],
60
+ capture_output=True,
61
+ text=True,
62
+ timeout=180,
63
  )
64
  if result.returncode != 0:
65
  raise RuntimeError(f"pdftoppm failed: {result.stderr.strip()}")
66
 
67
  images = sorted(glob.glob(os.path.join(pages_dir, "*.png")))
68
  if not images:
69
+ raise RuntimeError("No page images could be extracted from the PDF.")
70
  return images
71
 
72
 
73
+ def ocr_images(image_paths: list[str], model_name: str, progress: gr.Progress) -> list[str]:
74
+ processor, model = get_trocr_model(model_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  page_texts = []
76
 
77
+ total_pages = len(image_paths)
78
+ for idx, img_path in enumerate(image_paths):
79
+ progress((0.1 + (idx / total_pages) * 0.5), desc=f"OCR processing: page {idx + 1}/{total_pages}")
80
+
81
  image = Image.open(img_path).convert("RGB")
82
  pixel_values = processor(images=image, return_tensors="pt").pixel_values
83
+
84
  with torch.no_grad():
85
+ generated_ids = model.generate(pixel_values)
86
+
87
+ text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
88
  page_texts.append(text.strip())
89
 
90
  return page_texts
91
 
92
 
93
+ def refine_grammar(raw_pages: list[str], progress: gr.Progress) -> list[str]:
94
+ pipe = get_grammar_pipeline()
95
+ refined_pages = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ total_pages = len(raw_pages)
98
+ for idx, page in enumerate(raw_pages):
99
+ progress((0.6 + (idx / total_pages) * 0.3), desc=f"Refining grammar: page {idx + 1}/{total_pages}")
100
+
101
+ if not page.strip():
102
+ refined_pages.append("")
103
+ continue
104
 
105
+ # Chunk text into 400-char segments for T5 context window
106
+ chunks = [page[i:i + 400] for i in range(0, len(page), 400)]
 
 
 
107
  corrected_chunks = []
108
+
109
  for chunk in chunks:
110
  if not chunk.strip():
111
  continue
 
116
  early_stopping=True,
117
  )
118
  corrected_chunks.append(out[0]["generated_text"])
 
 
 
119
 
120
+ refined_pages.append(" ".join(corrected_chunks) if corrected_chunks else page)
121
 
122
+ return refined_pages
 
 
123
 
 
 
 
 
124
 
125
+ def export_to_docx(pages: list[str], output_path: str):
126
  doc = Document()
127
+
128
  for section in doc.sections:
129
+ section.top_margin = Inches(1)
130
  section.bottom_margin = Inches(1)
131
+ section.left_margin = Inches(1)
132
+ section.right_margin = Inches(1)
133
 
134
+ heading = doc.add_heading("Converted Handwritten Document", level=1)
135
+ heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
136
+ doc.add_paragraph()
137
 
138
+ for idx, page_text in enumerate(pages):
139
+ if idx > 0:
140
  doc.add_page_break()
141
+
142
+ lines = page_text.split("\n")
143
+ for line in lines:
144
+ line_str = line.strip()
145
  p = doc.add_paragraph()
146
+ if line_str:
147
+ run = p.add_run(line_str)
148
+ run.font.name = "Calibri"
149
  run.font.size = Pt(12)
150
 
151
  doc.save(output_path)
152
 
153
 
154
  # ───────────��──────────────────────────────────────────────────────────────────
155
+ # Main Pipeline Executable
156
  # ──────────────────────────────────────────────────────────────────────────────
157
 
158
+ def convert_pdf(pdf_file, trocr_model: str, use_grammar: bool, progress=gr.Progress()):
 
 
 
 
 
159
  if pdf_file is None:
160
+ raise gr.Error("Please upload a handwritten PDF file.")
161
 
162
+ # Cross-version filepath handling
163
+ pdf_path = pdf_file if isinstance(pdf_file, str) else getattr(pdf_file, "name", str(pdf_file))
164
 
165
+ with tempfile.TemporaryDirectory() as tmp_dir:
166
+ # Step 1: Convert PDF to PNGs
167
+ progress(0.05, desc="Extracting pages from PDF...")
168
+ images = pdf_to_images(pdf_path, tmp_dir)
169
 
170
+ # Step 2: OCR with TrOCR
171
+ progress(0.10, desc="Loading OCR Model...")
172
+ raw_pages = ocr_images(images, trocr_model, progress)
173
+ raw_text_combined = "\n\n--- [PAGE BREAK] ---\n\n".join(raw_pages)
174
 
175
+ # Step 3: Optional T5 Grammar Correction
176
  if use_grammar:
177
+ progress(0.60, desc="Loading Grammar Model...")
178
+ final_pages = refine_grammar(raw_pages, progress)
179
  else:
180
  final_pages = raw_pages
181
 
182
+ final_text_combined = "\n\n--- [PAGE BREAK] ---\n\n".join(final_pages)
 
 
 
183
 
184
+ # Step 4: Write .docx File
185
+ progress(0.92, desc="Creating Word Document...")
186
+ docx_temp_path = os.path.join(tmp_dir, "ScriptFlow_Output.docx")
187
+ export_to_docx(final_pages, docx_temp_path)
188
+
189
+ # Export file out of temporary scope for downloading
190
+ output_file = tempfile.NamedTemporaryFile(suffix=".docx", delete=False, prefix="scriptflow_")
191
+ output_file.close()
192
+ shutil.copy(docx_temp_path, output_file.name)
193
 
194
+ progress(1.0, desc="Conversion Complete!")
195
+ return raw_text_combined, final_text_combined, output_file.name
 
196
 
197
 
198
  # ──────────────────────────────────────────────────────────────────────────────
199
+ # Gradio UI Design
200
  # ──────────────────────────────────────────────────────────────────────────────
201
 
202
+ with gr.Blocks(title="ScriptFlow — PDF to Word Converter", theme=gr.themes.Soft()) as demo:
203
  gr.Markdown(
204
  """
205
  # ✍️ ScriptFlow
206
+ ### Convert Handwritten PDFs to Word (.docx) Documents
207
+ *Extract handwritten text with TrOCR, clean up syntax with T5, and download a ready-to-edit `.docx` file.*
208
  """
209
  )
210
 
211
  with gr.Row():
212
  with gr.Column(scale=1):
213
  pdf_input = gr.File(
214
+ label="Upload Handwritten PDF",
215
  file_types=[".pdf"],
216
  type="filepath",
217
  )
218
+ model_selector = gr.Radio(
219
  choices=[
220
+ ("Small (~330 MB) — Faster", "microsoft/trocr-small-handwritten"),
221
+ ("Base (~440 MB) — More Accurate", "microsoft/trocr-base-handwritten"),
222
  ],
223
  value="microsoft/trocr-small-handwritten",
224
+ label="TrOCR Model",
225
  )
226
+ grammar_toggle = gr.Checkbox(
227
+ label="Enable T5 Grammar & Refinement",
228
  value=False,
229
  )
230
+ convert_btn = gr.Button("Convert Document", variant="primary")
231
 
232
  with gr.Column(scale=2):
233
+ raw_output = gr.Textbox(label="Raw OCR Output", lines=10, interactive=False)
234
+ refined_output = gr.Textbox(label="Refined Output", lines=10, interactive=False)
235
+ docx_download = gr.File(label="Download Word (.docx) File", interactive=False)
236
 
237
  gr.Markdown(
238
  """
239
  ---
240
+ **Tips for best results:**
241
+ * High-resolution scans (200+ DPI) work significantly better.
242
+ * Grammar correction adds extra processing time per page on CPU.
 
243
  """
244
  )
245
 
246
+ convert_btn.click(
247
+ fn=convert_pdf,
248
+ inputs=[pdf_input, model_selector, grammar_toggle],
249
+ outputs=[raw_output, refined_output, docx_download],
250
  )
251
 
252
  if __name__ == "__main__":