DamLoan commited on
Commit
17e5f34
·
verified ·
1 Parent(s): 9c0ece9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +467 -83
app.py CHANGED
@@ -1,93 +1,477 @@
1
- import os
2
  import gradio as gr
3
- import tempfile
4
- import shutil
5
  import pandas as pd
 
6
  from PIL import Image
7
- from preprocess import convert_pdf_to_images, preprocess_image
8
- from llm_utils import load_image, generate_response
9
-
10
- # Global temporary directory for image processing
11
- temp_dir = tempfile.mkdtemp()
12
-
13
- pdf_image_map = {} # Map from PDF to its image list
14
- image_preview_map = {} # Map from image name to full path
15
-
16
-
17
- def extract_images_from_pdfs(pdf_files):
18
- global pdf_image_map, image_preview_map
19
- pdf_image_map.clear()
20
- image_preview_map.clear()
21
-
22
- previews = []
23
-
24
- for pdf in pdf_files:
25
- image_paths = convert_pdf_to_images(pdf.name, temp_dir)
26
- pdf_image_map[pdf.name] = image_paths
27
-
28
- for img_path in image_paths:
29
- img_name = os.path.basename(img_path)
30
- image_preview_map[img_name] = img_path
31
- previews.append((img_name, img_path))
32
-
33
- # Return preview (tuples: filename, image_path)
34
- return [img for _, img in previews]
35
-
36
-
37
- def process_selected_images(selected_images, output_excel_name):
38
- results = []
39
-
40
- for img_name in selected_images:
41
- img_path = image_preview_map.get(img_name)
42
- if img_path is None:
43
- continue
44
-
45
- processed = preprocess_image(img_path)
46
- processed_path = os.path.join(temp_dir, f"processed_{img_name}")
47
- Image.fromarray(processed).save(processed_path)
48
-
49
- # Run LLM
50
- pixel_values = load_image(processed_path)
51
- response = generate_response(pixel_values)
52
-
53
- # Clean filename to find source pdf
54
- base_name = img_name.rsplit("_page_", 1)[0] + ".pdf"
55
- results.append({"Source PDF": base_name, "Page Image": img_name, "LLM Output": response})
56
-
57
- df = pd.DataFrame(results)
58
- output_excel = os.path.join(temp_dir, output_excel_name)
59
- df.to_excel(output_excel, index=False)
60
- return output_excel
61
-
62
-
63
- def reset_all():
64
- shutil.rmtree(temp_dir)
65
- os.makedirs(temp_dir, exist_ok=True)
66
-
67
-
68
- with gr.Blocks() as demo:
69
- gr.Markdown("## 🧠 PDF → Image → LLM → Excel Output")
70
-
71
- with gr.Row():
72
- pdf_input = gr.File(file_types=[".pdf"], file_count="multiple", label="📎 Upload multiple PDF files")
73
- extract_btn = gr.Button("🔍 Extract Images")
74
 
75
- gallery = gr.Gallery(label="🖼️ Choose images to process", columns=4, allow_preview=True, interactive=True, show_label=True).style(grid=4)
76
- selected_images = gr.CheckboxGroup(choices=[], label="Select image filenames for processing")
 
77
 
78
- with gr.Row():
79
- output_name = gr.Textbox(label="📄 Output Excel filename", value="output.xlsx")
80
- generate_btn = gr.Button("🚀 Generate Excel")
81
 
82
- excel_output = gr.File(label="📥 Download Excel")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- def update_gallery(pdf_files):
85
- previews = extract_images_from_pdfs(pdf_files)
86
- choices = list(image_preview_map.keys())
87
- return gr.update(value=previews), gr.update(choices=choices, value=[])
88
 
89
- extract_btn.click(update_gallery, inputs=[pdf_input], outputs=[gallery, selected_images])
90
- generate_btn.click(fn=process_selected_images, inputs=[selected_images, output_name], outputs=[excel_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
 
92
  if __name__ == "__main__":
93
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
2
  import pandas as pd
3
+ import numpy as np
4
  from PIL import Image
5
+ import io
6
+ import tempfile
7
+ import os
8
+ import zipfile
9
+ from pathlib import Path
10
+ import json
11
+ from typing import List, Dict, Any, Tuple, Optional
12
+ import logging
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ # Import your custom modules
15
+ from preprocess import PDFImageProcessor, process_uploaded_pdf
16
+ from llm_utils import load_model, extract_info_from_image, cleanup_model
17
 
18
+ # Setup logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
 
22
+ class PDFProcessingApp:
23
+ def __init__(self):
24
+ self.processor = PDFImageProcessor()
25
+ self.model = None
26
+ self.tokenizer = None
27
+ self.current_images = {} # Store images by PDF filename
28
+ self.processed_results = {} # Store OCR results
29
+
30
+ def load_models(self):
31
+ """Load ML models on startup"""
32
+ try:
33
+ if self.model is None:
34
+ logger.info("Loading models...")
35
+ self.model, self.tokenizer = load_model()
36
+ logger.info("Models loaded successfully!")
37
+ except Exception as e:
38
+ logger.error(f"Error loading models: {e}")
39
+ raise
40
+
41
+ def process_multiple_pdfs(self, pdf_files: List[Any]) -> Tuple[Dict, str]:
42
+ """Process multiple PDF files and extract images"""
43
+ if not pdf_files:
44
+ return {}, "❌ Vui lòng upload ít nhất một file PDF"
45
+
46
+ self.current_images = {}
47
+ pdf_info = {}
48
+
49
+ try:
50
+ for pdf_file in pdf_files:
51
+ filename = Path(pdf_file.name).stem
52
+ logger.info(f"Processing PDF: {filename}")
53
+
54
+ # Read PDF bytes
55
+ pdf_bytes = pdf_file.read()
56
+
57
+ # Convert PDF to images
58
+ images = self.processor.pdf_to_images(pdf_bytes)
59
+
60
+ # Store images
61
+ self.current_images[filename] = images
62
+ pdf_info[filename] = {
63
+ 'total_pages': len(images),
64
+ 'filename': filename
65
+ }
66
+
67
+ logger.info(f"Extracted {len(images)} pages from {filename}")
68
+
69
+ # Create gallery data for display
70
+ gallery_data = self.create_gallery_data()
71
+
72
+ info_text = f"✅ Đã xử lý {len(pdf_files)} file PDF, tổng {sum(info['total_pages'] for info in pdf_info.values())} trang"
73
+
74
+ return gallery_data, info_text
75
+
76
+ except Exception as e:
77
+ logger.error(f"Error processing PDFs: {e}")
78
+ return {}, f"❌ Lỗi xử lý PDF: {str(e)}"
79
+
80
+ def create_gallery_data(self) -> Dict:
81
+ """Create data structure for image gallery"""
82
+ gallery_data = {}
83
+
84
+ for pdf_name, images in self.current_images.items():
85
+ gallery_images = []
86
+ for i, img_array in enumerate(images):
87
+ # Convert numpy array to PIL Image
88
+ if img_array.dtype != np.uint8:
89
+ img_array = (img_array * 255).astype(np.uint8)
90
+
91
+ if len(img_array.shape) == 2: # Grayscale
92
+ pil_img = Image.fromarray(img_array, mode='L')
93
+ else: # Color
94
+ pil_img = Image.fromarray(img_array)
95
+
96
+ # Resize for display (keeping aspect ratio)
97
+ pil_img.thumbnail((300, 400), Image.Resampling.LANCZOS)
98
+ gallery_images.append(pil_img)
99
+
100
+ gallery_data[pdf_name] = gallery_images
101
+
102
+ return gallery_data
103
+
104
+ def update_image_gallery(self, pdf_files):
105
+ """Update the image gallery when PDFs are uploaded"""
106
+ if not pdf_files:
107
+ return gr.update(value=[], visible=False), gr.update(visible=False), ""
108
+
109
+ gallery_data, info_text = self.process_multiple_pdfs(pdf_files)
110
+
111
+ if not gallery_data:
112
+ return gr.update(value=[], visible=False), gr.update(visible=False), info_text
113
+
114
+ # Flatten all images for gallery display
115
+ all_images = []
116
+ image_metadata = []
117
+
118
+ for pdf_name, images in gallery_data.items():
119
+ for i, img in enumerate(images):
120
+ all_images.append(img)
121
+ image_metadata.append({
122
+ 'pdf_name': pdf_name,
123
+ 'page_num': i + 1,
124
+ 'display_name': f"{pdf_name} - Trang {i + 1}"
125
+ })
126
+
127
+ # Store metadata for later use
128
+ self.image_metadata = image_metadata
129
+
130
+ return (
131
+ gr.update(value=all_images, visible=True),
132
+ gr.update(visible=True),
133
+ info_text
134
+ )
135
+
136
+ def parse_range_input(self, range_text: str, total_images: int) -> List[int]:
137
+ """Parse range input like '1-5, 8, 10-12' to list of indices"""
138
+ if not range_text.strip():
139
+ return []
140
+
141
+ indices = []
142
+ parts = range_text.split(',')
143
+
144
+ for part in parts:
145
+ part = part.strip()
146
+ if '-' in part:
147
+ # Range like "1-5"
148
+ try:
149
+ start, end = map(int, part.split('-'))
150
+ indices.extend(range(max(1, start) - 1, min(total_images, end)))
151
+ except:
152
+ continue
153
+ else:
154
+ # Single number
155
+ try:
156
+ num = int(part)
157
+ if 1 <= num <= total_images:
158
+ indices.append(num - 1)
159
+ except:
160
+ continue
161
+
162
+ return sorted(list(set(indices))) # Remove duplicates and sort
163
+
164
+ def process_selected_images(self, selected_indices: List[int], range_input: str,
165
+ custom_prompt: str) -> Tuple[str, Any]:
166
+ """Process selected images with OCR"""
167
+ if not hasattr(self, 'image_metadata'):
168
+ return "❌ Vui lòng upload PDF trước", None
169
+
170
+ # Load models if not loaded
171
+ if self.model is None:
172
+ try:
173
+ self.load_models()
174
+ except Exception as e:
175
+ return f"❌ Lỗi tải model: {str(e)}", None
176
+
177
+ # Combine selected indices from gallery and range input
178
+ total_images = len(self.image_metadata)
179
+ range_indices = self.parse_range_input(range_input, total_images)
180
+ all_selected = sorted(list(set(selected_indices + range_indices)))
181
+
182
+ if not all_selected:
183
+ return "❌ Vui lòng chọn ít nhất một ảnh để xử lý", None
184
+
185
+ logger.info(f"Processing {len(all_selected)} selected images")
186
+
187
+ # Group by PDF for organized results
188
+ pdf_results = {}
189
+ processed_count = 0
190
+
191
+ try:
192
+ for idx in all_selected:
193
+ if idx >= len(self.image_metadata):
194
+ continue
195
+
196
+ metadata = self.image_metadata[idx]
197
+ pdf_name = metadata['pdf_name']
198
+ page_num = metadata['page_num']
199
+
200
+ # Get the image
201
+ img_array = self.current_images[pdf_name][page_num - 1]
202
+
203
+ # Preprocess image
204
+ processed_img = self.processor.preprocess_image(img_array)
205
+
206
+ # Save to temp file for OCR
207
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp_file:
208
+ pil_img = Image.fromarray(processed_img)
209
+ pil_img.save(tmp_file.name)
210
+
211
+ # Extract text using OCR
212
+ try:
213
+ result = extract_info_from_image(
214
+ tmp_file.name,
215
+ self.model,
216
+ self.tokenizer,
217
+ custom_prompt=custom_prompt if custom_prompt.strip() else None
218
+ )
219
+
220
+ # Store result
221
+ if pdf_name not in pdf_results:
222
+ pdf_results[pdf_name] = []
223
+
224
+ pdf_results[pdf_name].append({
225
+ 'page': page_num,
226
+ 'content': result
227
+ })
228
+
229
+ processed_count += 1
230
+ logger.info(f"Processed page {page_num} of {pdf_name}")
231
+
232
+ except Exception as e:
233
+ logger.error(f"OCR error for {pdf_name} page {page_num}: {e}")
234
+ if pdf_name not in pdf_results:
235
+ pdf_results[pdf_name] = []
236
+ pdf_results[pdf_name].append({
237
+ 'page': page_num,
238
+ 'content': f"Lỗi xử lý: {str(e)}"
239
+ })
240
+
241
+ # Clean up temp file
242
+ try:
243
+ os.unlink(tmp_file.name)
244
+ except:
245
+ pass
246
+
247
+ # Create Excel files
248
+ excel_files = self.create_excel_outputs(pdf_results)
249
+
250
+ status_msg = f"✅ Đã xử lý {processed_count} ảnh từ {len(pdf_results)} file PDF"
251
+
252
+ return status_msg, excel_files
253
+
254
+ except Exception as e:
255
+ logger.error(f"Error in OCR processing: {e}")
256
+ return f"❌ Lỗi xử lý: {str(e)}", None
257
+
258
+ def create_excel_outputs(self, pdf_results: Dict) -> Any:
259
+ """Create Excel files for each PDF with OCR results"""
260
+ if not pdf_results:
261
+ return None
262
+
263
+ # Create a zip file containing all Excel files
264
+ zip_buffer = io.BytesIO()
265
+
266
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
267
+ for pdf_name, results in pdf_results.items():
268
+ # Create DataFrame for this PDF
269
+ df_data = []
270
+
271
+ for result in results:
272
+ page_num = result['page']
273
+ content = result['content']
274
+
275
+ # Try to parse the content into structured data
276
+ # This is a simple example - you might want to enhance this
277
+ df_data.append({
278
+ 'Trang': page_num,
279
+ 'Nội dung': content,
280
+ 'Thời gian xử lý': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')
281
+ })
282
+
283
+ # Create DataFrame
284
+ df = pd.DataFrame(df_data)
285
+
286
+ # Save to Excel in memory
287
+ excel_buffer = io.BytesIO()
288
+ with pd.ExcelWriter(excel_buffer, engine='openpyxl') as writer:
289
+ df.to_excel(writer, sheet_name='OCR_Results', index=False)
290
+
291
+ # Add to zip
292
+ excel_filename = f"{pdf_name}_OCR_Results.xlsx"
293
+ zip_file.writestr(excel_filename, excel_buffer.getvalue())
294
+
295
+ zip_buffer.seek(0)
296
+ return zip_buffer.getvalue()
297
 
298
+ # Initialize the app
299
+ app = PDFProcessingApp()
 
 
300
 
301
+ # Create Gradio interface
302
+ def create_interface():
303
+ with gr.Blocks(
304
+ title="PDF OCR Processor",
305
+ theme=gr.themes.Soft(),
306
+ css="""
307
+ .gradio-container {
308
+ max-width: 1200px !important;
309
+ }
310
+ .gallery-container {
311
+ max-height: 600px;
312
+ overflow-y: auto;
313
+ }
314
+ """
315
+ ) as demo:
316
+
317
+ gr.Markdown("""
318
+ # 📄 PDF OCR Processor
319
+
320
+ **Hướng dẫn sử dụng:**
321
+ 1. Upload nhiều file PDF cùng lúc
322
+ 2. Xem preview các trang và chọn trang cần xử lý OCR
323
+ 3. Tùy chỉnh prompt nếu cần
324
+ 4. Tải xuống kết quả Excel theo tên file gốc
325
+ """)
326
+
327
+ with gr.Tab("📤 Upload & Xử lý"):
328
+ # File upload section
329
+ with gr.Row():
330
+ pdf_files = gr.Files(
331
+ label="📁 Upload PDF Files (Có thể chọn nhiều file)",
332
+ file_types=[".pdf"],
333
+ file_count="multiple"
334
+ )
335
+
336
+ # Status and info
337
+ upload_status = gr.Textbox(
338
+ label="📊 Trạng thái",
339
+ interactive=False,
340
+ value="Chưa có file nào được upload"
341
+ )
342
+
343
+ # Image gallery and selection
344
+ with gr.Row():
345
+ with gr.Column(scale=2):
346
+ image_gallery = gr.Gallery(
347
+ label="📸 Preview các trang PDF (Click để chọn)",
348
+ show_label=True,
349
+ elem_classes=["gallery-container"],
350
+ columns=3,
351
+ rows=2,
352
+ height=400,
353
+ allow_preview=True,
354
+ selected_index=None,
355
+ visible=False
356
+ )
357
+
358
+ with gr.Column(scale=1):
359
+ selection_options = gr.Group(visible=False)
360
+
361
+ with selection_options:
362
+ gr.Markdown("### 🎯 Tùy chọn chọn ảnh")
363
+
364
+ range_input = gr.Textbox(
365
+ label="📝 Chọn theo range (VD: 1-5, 8, 10-12)",
366
+ placeholder="1-5, 8, 10-12",
367
+ info="Có thể kết hợp với việc click chọn ở gallery"
368
+ )
369
+
370
+ custom_prompt = gr.Textbox(
371
+ label="✏️ Custom Prompt (Tùy chọn)",
372
+ placeholder="Nhập prompt tùy chỉnh cho OCR...",
373
+ lines=3,
374
+ value="Trích xuất dữ liệu các cột: STT, Mã số thuế, Tên người nộp thuế, Địa chỉ, Số tiền thuế nợ, Biện pháp cưỡng chế. Hãy cố gắng đọc rõ những con số hoặc chữ bị đóng dấu và trả về dạng markdown."
375
+ )
376
+
377
+ process_btn = gr.Button(
378
+ "🚀 Bắt đầu xử lý OCR",
379
+ variant="primary",
380
+ size="lg"
381
+ )
382
+
383
+ # Results section
384
+ with gr.Row():
385
+ processing_status = gr.Textbox(
386
+ label="⚡ Kết quả xử lý",
387
+ interactive=False
388
+ )
389
+
390
+ with gr.Row():
391
+ download_files = gr.File(
392
+ label="📥 Tải xuống kết quả Excel",
393
+ visible=False
394
+ )
395
+
396
+ with gr.Tab("ℹ️ Hướng dẫn chi tiết"):
397
+ gr.Markdown("""
398
+ ## 📋 Hướng dẫn sử dụng chi tiết
399
+
400
+ ### 1. Upload PDF Files
401
+ - Click vào "📁 Upload PDF Files" để chọn nhiều file PDF
402
+ - Hệ thống sẽ tự động chuyển đổi tất cả các trang thành ảnh
403
+ - Xem trạng thái upload trong mục "📊 Trạng thái"
404
+
405
+ ### 2. Chọn trang cần xử lý
406
+ **Cách 1: Click chọn trong Gallery**
407
+ - Xem preview tất cả các trang trong "📸 Preview các trang PDF"
408
+ - Click vào các trang muốn xử lý (có thể chọn nhiều)
409
+ - Trang được chọn sẽ có viền xanh
410
+
411
+ **Cách 2: Nhập range**
412
+ - Sử dụng ô "📝 Chọn theo range"
413
+ - Định dạng: `1-5, 8, 10-12` (có thể kết hợp range và số đơn)
414
+ - Ví dụ: `1-3, 7, 10-15` sẽ chọn trang 1,2,3,7,10,11,12,13,14,15
415
+
416
+ **Cách 3: Kết hợp cả hai**
417
+ - Có thể vừa click trong gallery vừa nhập range
418
+ - Hệ thống sẽ gộp tất cả lựa chọn lại
419
+
420
+ ### 3. Tùy chỉnh Prompt (Tùy chọn)
421
+ - Mặc định: Trích xuất thông tin thuế
422
+ - Có thể thay đổi để phù hợp với nội dung PDF khác
423
+ - Ví dụ: "Trích xuất tất cả văn bản trong ảnh"
424
+
425
+ ### 4. Xử lý và Tải xuống
426
+ - Click "🚀 Bắt đầu xử lý OCR"
427
+ - Xem tiến trình trong "⚡ Kết quả xử lý"
428
+ - Tải file Excel kết quả (mỗi PDF sẽ có file Excel riêng)
429
+
430
+ ### 5. Kết quả
431
+ - File Excel sẽ có tên giống file PDF gốc
432
+ - Mỗi trang được xử lý sẽ là một dòng trong Excel
433
+ - Có thông tin trang số và nội dung OCR
434
+
435
+ ## 🔧 Lưu ý kỹ thuật
436
+ - Hỗ trợ PDF scan và PDF có ảnh
437
+ - Tự động tiền xử lý ảnh để tăng độ chính xác OCR
438
+ - Sử dụng AI model Vintern-1B-v3_5 cho OCR tiếng Việt
439
+ - Kết quả trả về dạng Markdown để dễ đọc
440
+ """)
441
+
442
+ # Event handlers
443
+ pdf_files.change(
444
+ fn=app.update_image_gallery,
445
+ inputs=[pdf_files],
446
+ outputs=[image_gallery, selection_options, upload_status]
447
+ )
448
+
449
+ process_btn.click(
450
+ fn=app.process_selected_images,
451
+ inputs=[image_gallery, range_input, custom_prompt],
452
+ outputs=[processing_status, download_files]
453
+ ).then(
454
+ fn=lambda: gr.update(visible=True),
455
+ outputs=[download_files]
456
+ )
457
+
458
+ return demo
459
 
460
+ # Launch the app
461
  if __name__ == "__main__":
462
+ # Pre-load models for faster processing
463
+ try:
464
+ logger.info("Pre-loading models...")
465
+ app.load_models()
466
+ except Exception as e:
467
+ logger.warning(f"Could not pre-load models: {e}")
468
+
469
+ # Create and launch interface
470
+ demo = create_interface()
471
+ demo.launch(
472
+ server_name="0.0.0.0",
473
+ server_port=7860,
474
+ share=True,
475
+ show_error=True,
476
+ debug=True
477
+ )