github-actions[bot] commited on
Commit
a7f1144
·
1 Parent(s): 4b2c656

Sync from GitHub: cff9d2103d559b06cd5ccba9969757ff860436aa

Browse files
Files changed (6) hide show
  1. .gitignore +2 -13
  2. Dockerfile +1 -18
  3. app.py +8 -14
  4. config.py +0 -5
  5. frontend/src/components/ResultCard.jsx +1 -39
  6. inference.py +13 -70
.gitignore CHANGED
@@ -31,21 +31,10 @@ htmlcov/
31
  frontend/node_modules/
32
  frontend/.env.local
33
 
34
- # Documentation (keep essential ones)
35
  *.md
36
  !README_git.md
37
  !README.md
38
- DEPLOYMENT.md
39
- HF_DEPLOYMENT_READY.md
40
- IMAGE_ENHANCEMENT.md
41
-
42
- # Test files and docs
43
- test_*.py
44
- Docs/
45
-
46
- # Executables and examples
47
  executable.py
48
  client_example.py
49
-
50
- # Real-ESRGAN downloaded binaries (will be installed via Docker)
51
- utils/realesrgan/
 
31
  frontend/node_modules/
32
  frontend/.env.local
33
 
 
34
  *.md
35
  !README_git.md
36
  !README.md
37
+ test*
 
 
 
 
 
 
 
 
38
  executable.py
39
  client_example.py
40
+ Docs
 
 
Dockerfile CHANGED
@@ -2,7 +2,7 @@ FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
5
- # Install system dependencies including Node.js and tools for Real-ESRGAN
6
  RUN apt-get update && apt-get install -y \
7
  git \
8
  libgl1 \
@@ -12,10 +12,6 @@ RUN apt-get update && apt-get install -y \
12
  libxrender-dev \
13
  libgomp1 \
14
  curl \
15
- wget \
16
- unzip \
17
- libvulkan1 \
18
- libvulkan-dev \
19
  && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
20
  && apt-get install -y nodejs \
21
  && rm -rf /var/lib/apt/lists/*
@@ -41,19 +37,6 @@ COPY inference.py .
41
  COPY app.py .
42
  COPY utils/ utils/
43
 
44
- # Download and setup Real-ESRGAN-ncnn-vulkan for image enhancement (after copying utils/)
45
- RUN mkdir -p /app/utils/realesrgan && \
46
- cd /app/utils/realesrgan && \
47
- wget -q https://github.com/xinntao/Real-ESRGAN-ncnn-vulkan/releases/download/v0.2.0/realesrgan-ncnn-vulkan-v0.2.0-ubuntu.zip && \
48
- unzip -q realesrgan-ncnn-vulkan-v0.2.0-ubuntu.zip && \
49
- rm realesrgan-ncnn-vulkan-v0.2.0-ubuntu.zip && \
50
- find . -name "realesrgan-ncnn-vulkan" -type f -exec chmod +x {} \; && \
51
- mv realesrgan-ncnn-vulkan-v0.2.0-ubuntu/* . 2>/dev/null || true && \
52
- rmdir realesrgan-ncnn-vulkan-v0.2.0-ubuntu 2>/dev/null || true && \
53
- echo "Real-ESRGAN installed at /app/utils/realesrgan/" && \
54
- ls -la /app/utils/realesrgan/ && \
55
- test -f /app/utils/realesrgan/realesrgan-ncnn-vulkan && echo "✓ Executable found" || echo "✗ Executable NOT found"
56
-
57
  # Expose Hugging Face Spaces default port
58
  EXPOSE 7860
59
 
 
2
 
3
  WORKDIR /app
4
 
5
+ # Install system dependencies including Node.js
6
  RUN apt-get update && apt-get install -y \
7
  git \
8
  libgl1 \
 
12
  libxrender-dev \
13
  libgomp1 \
14
  curl \
 
 
 
 
15
  && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
16
  && apt-get install -y nodejs \
17
  && rm -rf /var/lib/apt/lists/*
 
37
  COPY app.py .
38
  COPY utils/ utils/
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  # Expose Hugging Face Spaces default port
41
  EXPOSE 7860
42
 
app.py CHANGED
@@ -98,8 +98,7 @@ async def health_check():
98
  @app.post("/extract")
99
  async def extract_invoice(
100
  file: UploadFile = File(..., description="Invoice image file (JPG, PNG, JPEG)"),
101
- doc_id: Optional[str] = Form(None, description="Optional document identifier"),
102
- enhance: Optional[bool] = Form(None, description="Enable image enhancement (default: True)")
103
  ):
104
  """
105
  Extract information from invoice image
@@ -107,7 +106,6 @@ async def extract_invoice(
107
  **Parameters:**
108
  - **file**: Invoice image file (required)
109
  - **doc_id**: Optional document identifier (auto-generated from filename if not provided)
110
- - **enhance**: Enable image enhancement for blurry images (default: True)
111
 
112
  **Returns:**
113
  - JSON with extracted fields, confidence scores, and metadata
@@ -172,8 +170,8 @@ async def extract_invoice(
172
  if doc_id is None:
173
  doc_id = os.path.splitext(file.filename)[0]
174
 
175
- # Process invoice (with optional enhancement)
176
- result = InferenceProcessor.process_invoice(temp_file, doc_id, enhance=enhance)
177
 
178
  # Add total request time (includes file I/O)
179
  result['total_request_time_sec'] = round(time.time() - request_start, 2)
@@ -201,8 +199,7 @@ async def extract_invoice(
201
 
202
  @app.post("/process-invoice")
203
  async def process_invoice(
204
- file: UploadFile = File(..., description="Invoice image file"),
205
- enhance: Optional[bool] = Form(None, description="Enable image enhancement (default: True)")
206
  ):
207
  """
208
  Process a single invoice and return extracted information
@@ -210,7 +207,6 @@ async def process_invoice(
210
 
211
  **Parameters:**
212
  - **file**: Invoice image file (required)
213
- - **enhance**: Enable image enhancement for blurry images (default: True)
214
 
215
  **Returns:**
216
  - JSON with extracted_text, signature_coords, stamp_coords
@@ -241,8 +237,8 @@ async def process_invoice(
241
  # Use filename as doc_id
242
  doc_id = os.path.splitext(file.filename)[0] if file.filename else "invoice"
243
 
244
- # Process invoice (with optional enhancement)
245
- result = InferenceProcessor.process_invoice(temp_file, doc_id, enhance=enhance)
246
 
247
  # Extract fields from result
248
  fields = result.get("fields", {})
@@ -307,15 +303,13 @@ async def process_invoice(
307
 
308
  @app.post("/extract_batch")
309
  async def extract_batch(
310
- files: list[UploadFile] = File(..., description="Multiple invoice images"),
311
- enhance: Optional[bool] = Form(None, description="Enable image enhancement (default: True)")
312
  ):
313
  """
314
  Extract information from multiple invoice images
315
 
316
  **Parameters:**
317
  - **files**: List of invoice image files
318
- - **enhance**: Enable image enhancement for blurry images (default: True)
319
 
320
  **Returns:**
321
  - JSON array with results for each invoice
@@ -350,7 +344,7 @@ async def extract_batch(
350
  # Process
351
  try:
352
  doc_id = os.path.splitext(file.filename)[0]
353
- result = InferenceProcessor.process_invoice(temp_file, doc_id, enhance=enhance)
354
  results.append(result)
355
  except Exception as e:
356
  results.append({
 
98
  @app.post("/extract")
99
  async def extract_invoice(
100
  file: UploadFile = File(..., description="Invoice image file (JPG, PNG, JPEG)"),
101
+ doc_id: Optional[str] = Form(None, description="Optional document identifier")
 
102
  ):
103
  """
104
  Extract information from invoice image
 
106
  **Parameters:**
107
  - **file**: Invoice image file (required)
108
  - **doc_id**: Optional document identifier (auto-generated from filename if not provided)
 
109
 
110
  **Returns:**
111
  - JSON with extracted fields, confidence scores, and metadata
 
170
  if doc_id is None:
171
  doc_id = os.path.splitext(file.filename)[0]
172
 
173
+ # Process invoice
174
+ result = InferenceProcessor.process_invoice(temp_file, doc_id)
175
 
176
  # Add total request time (includes file I/O)
177
  result['total_request_time_sec'] = round(time.time() - request_start, 2)
 
199
 
200
  @app.post("/process-invoice")
201
  async def process_invoice(
202
+ file: UploadFile = File(..., description="Invoice image file")
 
203
  ):
204
  """
205
  Process a single invoice and return extracted information
 
207
 
208
  **Parameters:**
209
  - **file**: Invoice image file (required)
 
210
 
211
  **Returns:**
212
  - JSON with extracted_text, signature_coords, stamp_coords
 
237
  # Use filename as doc_id
238
  doc_id = os.path.splitext(file.filename)[0] if file.filename else "invoice"
239
 
240
+ # Process invoice
241
+ result = InferenceProcessor.process_invoice(temp_file, doc_id)
242
 
243
  # Extract fields from result
244
  fields = result.get("fields", {})
 
303
 
304
  @app.post("/extract_batch")
305
  async def extract_batch(
306
+ files: list[UploadFile] = File(..., description="Multiple invoice images")
 
307
  ):
308
  """
309
  Extract information from multiple invoice images
310
 
311
  **Parameters:**
312
  - **files**: List of invoice image files
 
313
 
314
  **Returns:**
315
  - JSON array with results for each invoice
 
344
  # Process
345
  try:
346
  doc_id = os.path.splitext(file.filename)[0]
347
+ result = InferenceProcessor.process_invoice(temp_file, doc_id)
348
  results.append(result)
349
  except Exception as e:
350
  results.append({
config.py CHANGED
@@ -26,11 +26,6 @@ QUANTIZATION_CONFIG = {
26
  # Image processing settings
27
  MAX_IMAGE_SIZE = 800 # Maximum dimension for resizing
28
 
29
- # Image Enhancement Settings (Real-ESRGAN)
30
- ENABLE_IMAGE_ENHANCEMENT = True # Enable/disable image enhancement
31
- ENHANCEMENT_SCALE = 2 # Upscaling factor (2, 3, or 4)
32
- ENHANCEMENT_MODEL = "realesrgan-x4plus" # Model: realesrgan-x4plus, realesrgan-x4plus-anime, realesrnet-x4plus
33
-
34
  # Detection thresholds
35
  YOLO_CONFIDENCE_THRESHOLD = 0.25
36
 
 
26
  # Image processing settings
27
  MAX_IMAGE_SIZE = 800 # Maximum dimension for resizing
28
 
 
 
 
 
 
29
  # Detection thresholds
30
  YOLO_CONFIDENCE_THRESHOLD = 0.25
31
 
frontend/src/components/ResultCard.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import React, { useRef, useEffect, useState } from 'react';
2
- import { SlidersHorizontal, Download, Eye } from 'lucide-react';
3
 
4
  const ResultCard = ({ result, imageData, processedImageData, onReprocess, isProcessing }) => {
5
  const canvasRef = useRef(null);
@@ -11,7 +11,6 @@ const ResultCard = ({ result, imageData, processedImageData, onReprocess, isProc
11
  const [adjustedDataUrl, setAdjustedDataUrl] = useState(null);
12
  const [previewDimensions, setPreviewDimensions] = useState({ width: 0, height: 0 });
13
  const [currentImageData, setCurrentImageData] = useState(processedImageData || imageData);
14
- const [showEnhanced, setShowEnhanced] = useState(false);
15
 
16
  // Function to crop image regions
17
  const cropRegion = (img, coords, scaleX, scaleY) => {
@@ -254,43 +253,6 @@ const ResultCard = ({ result, imageData, processedImageData, onReprocess, isProc
254
  <span>100% (Best)</span>
255
  </div>
256
  </div>
257
-
258
- {/* Enhanced Image Toggle & Download */}
259
- {result.image_enhanced && result.enhanced_image && (
260
- <div className="bg-green-50 rounded-lg p-4 shadow-sm border border-green-200">
261
- <div className="flex items-center justify-between mb-2">
262
- <div className="flex items-center gap-2">
263
- <svg className="w-5 h-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
264
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
265
- </svg>
266
- <span className="text-sm font-medium text-green-800">Image Enhanced (2x)</span>
267
- </div>
268
- <div className="flex gap-2">
269
- <button
270
- onClick={() => {
271
- const enhancedData = `data:image/png;base64,${result.enhanced_image}`;
272
- setCurrentImageData(showEnhanced ? imageData : enhancedData);
273
- setShowEnhanced(!showEnhanced);
274
- }}
275
- className="flex items-center gap-1 px-3 py-1 bg-green-600 hover:bg-green-700 text-white rounded text-xs font-medium transition-colors"
276
- >
277
- <Eye className="w-3 h-3" />
278
- {showEnhanced ? 'Show Original' : 'Show Enhanced'}
279
- </button>
280
- <a
281
- href={`data:image/png;base64,${result.enhanced_image}`}
282
- download={`enhanced_${result.filename || 'invoice'}.png`}
283
- className="flex items-center gap-1 px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium transition-colors"
284
- >
285
- <Download className="w-3 h-3" />
286
- Download
287
- </a>
288
- </div>
289
- </div>
290
- <p className="text-xs text-green-700">Real-ESRGAN enhancement applied before processing</p>
291
- </div>
292
- )}
293
-
294
  <div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center">
295
  <canvas ref={canvasRef} className="max-w-full h-auto rounded shadow-md" />
296
  {isProcessing && (
 
1
  import React, { useRef, useEffect, useState } from 'react';
2
+ import { SlidersHorizontal } from 'lucide-react';
3
 
4
  const ResultCard = ({ result, imageData, processedImageData, onReprocess, isProcessing }) => {
5
  const canvasRef = useRef(null);
 
11
  const [adjustedDataUrl, setAdjustedDataUrl] = useState(null);
12
  const [previewDimensions, setPreviewDimensions] = useState({ width: 0, height: 0 });
13
  const [currentImageData, setCurrentImageData] = useState(processedImageData || imageData);
 
14
 
15
  // Function to crop image regions
16
  const cropRegion = (img, coords, scaleX, scaleY) => {
 
253
  <span>100% (Best)</span>
254
  </div>
255
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  <div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center">
257
  <canvas ref={canvasRef} className="max-w-full h-auto rounded shadow-md" />
258
  {isProcessing && (
inference.py CHANGED
@@ -7,7 +7,6 @@ import time
7
  import json
8
  import codecs
9
  import re
10
- import os
11
  from PIL import Image
12
  from qwen_vl_utils import process_vision_info
13
  from typing import Dict, Tuple
@@ -16,13 +15,9 @@ from config import (
16
  MAX_IMAGE_SIZE,
17
  HP_VALID_RANGE,
18
  ASSET_COST_VALID_RANGE,
19
- COST_PER_GPU_HOUR,
20
- ENABLE_IMAGE_ENHANCEMENT,
21
- ENHANCEMENT_SCALE,
22
- ENHANCEMENT_MODEL
23
  )
24
  from model_manager import model_manager
25
- from utils.image_enhancer import get_enhancer
26
 
27
 
28
  EXTRACTION_PROMPT = """
@@ -69,47 +64,18 @@ class InferenceProcessor:
69
  """Handles VLM inference, validation, and result processing"""
70
 
71
  @staticmethod
72
- def preprocess_image(image_path: str, enhance: bool = None) -> Tuple[Image.Image, str]:
73
- """Load, enhance (optional), and resize image if needed
 
74
 
75
- Args:
76
- image_path: Path to input image
77
- enhance: Whether to enhance image quality before processing (None=use config default)
78
-
79
- Returns:
80
- Tuple of (PIL Image ready for VLM, path to image file for YOLO)
81
- """
82
- # Use config default if not specified
83
- if enhance is None:
84
- enhance = ENABLE_IMAGE_ENHANCEMENT
85
-
86
- # Step 1: Enhance image if enabled
87
- enhanced_path = image_path
88
-
89
- if enhance:
90
- try:
91
- enhancer = get_enhancer()
92
- enhanced_path = enhancer.enhance_image(
93
- image_path,
94
- scale=ENHANCEMENT_SCALE,
95
- model_name=ENHANCEMENT_MODEL
96
- )
97
- except Exception as e:
98
- print(f"⚠️ Enhancement failed: {str(e)}, using original image")
99
- enhanced_path = image_path
100
-
101
- # Step 2: Load image
102
- image = Image.open(enhanced_path).convert("RGB")
103
-
104
- # Step 3: Resize if too large
105
  if max(image.size) > MAX_IMAGE_SIZE:
106
  ratio = MAX_IMAGE_SIZE / max(image.size)
107
  new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
108
  image = image.resize(new_size, Image.LANCZOS)
109
  print(f"🔄 Image resized to {new_size}")
110
 
111
- # Return both PIL Image and path (path will be cleaned up by caller)
112
- return image, enhanced_path
113
 
114
  @staticmethod
115
  def run_vlm_extraction(image: Image.Image) -> Tuple[str, float]:
@@ -318,14 +284,13 @@ class InferenceProcessor:
318
  return validated, field_confidence, warnings
319
 
320
  @staticmethod
321
- def process_invoice(image_path: str, doc_id: str = None, enhance: bool = None) -> Dict:
322
  """
323
  Complete invoice processing pipeline
324
 
325
  Args:
326
  image_path: Path to invoice image
327
  doc_id: Document identifier (optional)
328
- enhance: Whether to enhance image (None=use config default)
329
 
330
  Returns:
331
  dict: Complete JSON output with all fields
@@ -338,27 +303,14 @@ class InferenceProcessor:
338
  import os
339
  doc_id = os.path.splitext(os.path.basename(image_path))[0]
340
 
341
- # Step 1: Preprocess image (with optional enhancement)
342
  t1 = time.time()
343
- image, enhanced_image_path = InferenceProcessor.preprocess_image(image_path, enhance=enhance)
344
  timing_breakdown['image_preprocessing'] = round(time.time() - t1, 3)
345
 
346
- # Save enhanced image info for response
347
- image_was_enhanced = (enhanced_image_path != image_path)
348
- enhanced_image_base64 = None
349
-
350
- if image_was_enhanced:
351
- # Convert enhanced image to base64 for response
352
- import base64
353
- try:
354
- with open(enhanced_image_path, 'rb') as f:
355
- enhanced_image_base64 = base64.b64encode(f.read()).decode('utf-8')
356
- except:
357
- pass
358
-
359
- # Step 2: YOLO Detection (use enhanced image path for consistency)
360
  t2 = time.time()
361
- signature_info, stamp_info, signature_conf, stamp_conf = model_manager.detect_sign_stamp(enhanced_image_path)
362
  timing_breakdown['yolo_detection'] = round(time.time() - t2, 3)
363
 
364
  # Step 3: VLM Extraction
@@ -366,17 +318,10 @@ class InferenceProcessor:
366
  vlm_output, vlm_latency = InferenceProcessor.run_vlm_extraction(image)
367
  timing_breakdown['vlm_inference'] = round(vlm_latency, 3)
368
 
369
- # Clean up image and enhanced file if it was created
370
  image.close()
371
  del image
372
 
373
- # Cleanup enhanced temp file if created
374
- if enhanced_image_path != image_path:
375
- try:
376
- os.unlink(enhanced_image_path)
377
- except:
378
- pass
379
-
380
  # Step 4: Parse JSON
381
  t4 = time.time()
382
  raw_json = InferenceProcessor.extract_json_from_output(vlm_output)
@@ -412,9 +357,7 @@ class InferenceProcessor:
412
  "processing_time_sec": round(total_time, 2),
413
  "timing_breakdown": timing_breakdown,
414
  "cost_estimate_usd": round(cost_estimate, 6),
415
- "warnings": warnings if warnings else None,
416
- "image_enhanced": image_was_enhanced,
417
- "enhanced_image": enhanced_image_base64 if image_was_enhanced else None
418
  }
419
 
420
  return result
 
7
  import json
8
  import codecs
9
  import re
 
10
  from PIL import Image
11
  from qwen_vl_utils import process_vision_info
12
  from typing import Dict, Tuple
 
15
  MAX_IMAGE_SIZE,
16
  HP_VALID_RANGE,
17
  ASSET_COST_VALID_RANGE,
18
+ COST_PER_GPU_HOUR
 
 
 
19
  )
20
  from model_manager import model_manager
 
21
 
22
 
23
  EXTRACTION_PROMPT = """
 
64
  """Handles VLM inference, validation, and result processing"""
65
 
66
  @staticmethod
67
+ def preprocess_image(image_path: str) -> Image.Image:
68
+ """Load and resize image if needed"""
69
+ image = Image.open(image_path).convert("RGB")
70
 
71
+ # Resize if too large
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  if max(image.size) > MAX_IMAGE_SIZE:
73
  ratio = MAX_IMAGE_SIZE / max(image.size)
74
  new_size = (int(image.size[0] * ratio), int(image.size[1] * ratio))
75
  image = image.resize(new_size, Image.LANCZOS)
76
  print(f"🔄 Image resized to {new_size}")
77
 
78
+ return image
 
79
 
80
  @staticmethod
81
  def run_vlm_extraction(image: Image.Image) -> Tuple[str, float]:
 
284
  return validated, field_confidence, warnings
285
 
286
  @staticmethod
287
+ def process_invoice(image_path: str, doc_id: str = None) -> Dict:
288
  """
289
  Complete invoice processing pipeline
290
 
291
  Args:
292
  image_path: Path to invoice image
293
  doc_id: Document identifier (optional)
 
294
 
295
  Returns:
296
  dict: Complete JSON output with all fields
 
303
  import os
304
  doc_id = os.path.splitext(os.path.basename(image_path))[0]
305
 
306
+ # Step 1: Preprocess image
307
  t1 = time.time()
308
+ image = InferenceProcessor.preprocess_image(image_path)
309
  timing_breakdown['image_preprocessing'] = round(time.time() - t1, 3)
310
 
311
+ # Step 2: YOLO Detection
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  t2 = time.time()
313
+ signature_info, stamp_info, signature_conf, stamp_conf = model_manager.detect_sign_stamp(image_path)
314
  timing_breakdown['yolo_detection'] = round(time.time() - t2, 3)
315
 
316
  # Step 3: VLM Extraction
 
318
  vlm_output, vlm_latency = InferenceProcessor.run_vlm_extraction(image)
319
  timing_breakdown['vlm_inference'] = round(vlm_latency, 3)
320
 
321
+ # Clean up image
322
  image.close()
323
  del image
324
 
 
 
 
 
 
 
 
325
  # Step 4: Parse JSON
326
  t4 = time.time()
327
  raw_json = InferenceProcessor.extract_json_from_output(vlm_output)
 
357
  "processing_time_sec": round(total_time, 2),
358
  "timing_breakdown": timing_breakdown,
359
  "cost_estimate_usd": round(cost_estimate, 6),
360
+ "warnings": warnings if warnings else None
 
 
361
  }
362
 
363
  return result