cheesecz commited on
Commit
bfb6745
·
verified ·
1 Parent(s): b198dc1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -332
app.py CHANGED
@@ -1,356 +1,92 @@
1
  import gradio as gr
2
- import json
3
- import re
4
- from datetime import datetime
5
  import cv2
6
  import numpy as np
7
- from PIL import Image
8
- import easyocr
9
- import requests
10
- from typing import Dict, List, Optional, Tuple
11
 
12
- # Initialize EasyOCR reader with Indonesian and English
13
- reader = easyocr.Reader(['id', 'en'], gpu=False)
14
 
15
- class IndonesianDocumentProcessor:
16
- def __init__(self):
17
- self.document_patterns = {
18
- 'ktp': {
19
- 'nik': r'(\d{16})',
20
- 'name': r'(?:nama|name)[:\s]*([A-Za-z\s]+)',
21
- 'birth_place': r'(?:tempat.*lahir|place.*birth)[:\s]*([A-Za-z\s]+)',
22
- 'birth_date': r'(\d{2}[-/]\d{2}[-/]\d{4})',
23
- 'gender': r'(?:jenis.*kelamin|gender)[:\s]*(laki-laki|perempuan|male|female)',
24
- 'address': r'(?:alamat|address)[:\s]*([A-Za-z0-9\s,./]+)',
25
- 'rt_rw': r'rt[/\s]*(\d+)[/\s]*rw[/\s]*(\d+)',
26
- 'religion': r'(?:agama|religion)[:\s]*([A-Za-z\s]+)',
27
- 'marital_status': r'(?:status.*perkawinan|marital)[:\s]*([A-Za-z\s]+)',
28
- 'occupation': r'(?:pekerjaan|occupation)[:\s]*([A-Za-z\s]+)'
29
- },
30
- 'bpjs': {
31
- 'card_number': r'(\d{13})',
32
- 'name': r'(?:nama|name)[:\s]*([A-Za-z\s]+)',
33
- 'birth_date': r'(\d{2}[-/]\d{2}[-/]\d{4})',
34
- 'valid_until': r'(?:berlaku.*hingga|valid.*until)[:\s]*(\d{2}[-/]\d{2}[-/]\d{4})',
35
- 'class': r'(?:kelas|class)[:\s]*([I-III]|[1-3])'
36
- },
37
- 'kk': {
38
- 'kk_number': r'(\d{16})',
39
- 'head_name': r'(?:kepala.*keluarga|head)[:\s]*([A-Za-z\s]+)',
40
- 'address': r'(?:alamat|address)[:\s]*([A-Za-z0-9\s,./]+)',
41
- 'rt_rw': r'rt[/\s]*(\d+)[/\s]*rw[/\s]*(\d+)',
42
- 'kelurahan': r'(?:kelurahan|village)[:\s]*([A-Za-z\s]+)',
43
- 'kecamatan': r'(?:kecamatan|district)[:\s]*([A-Za-z\s]+)'
44
- },
45
- 'medical_bill': {
46
- 'bill_number': r'(?:no.*invoice|bill.*no|nota)[:\s]*([A-Za-z0-9/-]+)',
47
- 'date': r'(\d{2}[-/]\d{2}[-/]\d{4})',
48
- 'patient_name': r'(?:nama.*pasien|patient.*name)[:\s]*([A-Za-z\s]+)',
49
- 'total_amount': r'(?:total|jumlah)[:\s]*(?:rp\.?\s*)?(\d{1,3}(?:[.,]\d{3})*)',
50
- 'hospital_name': r'(?:rumah.*sakit|hospital|klinik|clinic)[:\s]*([A-Za-z\s]+)'
51
- }
52
- }
53
-
54
- def preprocess_image(self, image: np.ndarray) -> np.ndarray:
55
- """Preprocess image for better OCR results"""
56
- # Convert PIL to numpy if needed
57
- if isinstance(image, Image.Image):
58
- image = np.array(image)
59
-
60
- # Convert to grayscale
61
- if len(image.shape) == 3:
62
- gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
63
- else:
64
- gray = image
65
-
66
- # Apply adaptive threshold
67
- thresh = cv2.adaptiveThreshold(
68
- gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
69
- )
70
-
71
- # Denoise
72
- denoised = cv2.fastNlMeansDenoising(thresh)
73
-
74
- return denoised
75
-
76
- def extract_text_with_positions(self, image) -> List[Tuple[str, List]]:
77
- """Extract text with bounding box positions"""
78
- processed_img = self.preprocess_image(image)
79
- results = reader.readtext(processed_img)
80
-
81
- text_data = []
82
- for (bbox, text, confidence) in results:
83
- if confidence > 0.5: # Filter low confidence text
84
- text_data.append((text.strip(), bbox))
85
-
86
- return text_data
87
-
88
- def classify_document(self, text_content: str) -> str:
89
- """Classify document type based on text content"""
90
- text_lower = text_content.lower()
91
-
92
- # Check for specific keywords
93
- if any(keyword in text_lower for keyword in ['kartu tanda penduduk', 'ktp', 'republik indonesia']):
94
- return 'ktp'
95
- elif any(keyword in text_lower for keyword in ['bpjs', 'kesehatan', 'jaminan kesehatan']):
96
- return 'bpjs'
97
- elif any(keyword in text_lower for keyword in ['kartu keluarga', 'kepala keluarga']):
98
- return 'kk'
99
- elif any(keyword in text_lower for keyword in ['invoice', 'bill', 'tagihan', 'rumah sakit', 'klinik']):
100
- return 'medical_bill'
101
- else:
102
- return 'unknown'
103
-
104
- def extract_fields(self, text_content: str, doc_type: str) -> Dict:
105
- """Extract specific fields based on document type"""
106
- if doc_type not in self.document_patterns:
107
- return {}
108
-
109
- patterns = self.document_patterns[doc_type]
110
- extracted_fields = {}
111
- confidence_scores = {}
112
 
113
- text_lower = text_content.lower()
 
 
114
 
115
- for field, pattern in patterns.items():
116
- matches = re.findall(pattern, text_lower, re.IGNORECASE | re.MULTILINE)
117
- if matches:
118
- if field == 'rt_rw' and len(matches[0]) == 2:
119
- extracted_fields[field] = f"{matches[0][0]}/{matches[0][1]}"
120
- else:
121
- extracted_fields[field] = matches[0].strip() if isinstance(matches[0], str) else matches[0]
122
- confidence_scores[field] = 0.8 # Base confidence for regex matches
123
 
124
- return {
125
- 'extracted_fields': extracted_fields,
126
- 'confidence_scores': confidence_scores
 
 
127
  }
128
-
129
- def process_document(self, image) -> Dict:
130
- """Main processing function"""
131
- start_time = datetime.now()
132
-
133
- try:
134
- # Extract text with positions
135
- text_data = self.extract_text_with_positions(image)
136
-
137
- # Combine all text for classification and extraction
138
- full_text = ' '.join([text for text, _ in text_data])
139
-
140
- # Classify document
141
- doc_type = self.classify_document(full_text)
142
-
143
- # Extract fields
144
- field_data = self.extract_fields(full_text, doc_type)
145
-
146
- processing_time = (datetime.now() - start_time).total_seconds()
147
-
148
- result = {
149
- 'success': True,
150
- 'document_type': doc_type,
151
- 'extracted_fields': field_data.get('extracted_fields', {}),
152
- 'confidence_scores': field_data.get('confidence_scores', {}),
153
- 'raw_text': full_text,
154
- 'processing_time_seconds': processing_time,
155
- 'timestamp': datetime.now().isoformat()
156
- }
157
-
158
- return result
159
-
160
- except Exception as e:
161
- return {
162
- 'success': False,
163
- 'error': str(e),
164
- 'timestamp': datetime.now().isoformat()
165
- }
166
-
167
- # Initialize processor
168
- processor = IndonesianDocumentProcessor()
169
-
170
- def process_uploaded_image(image):
171
- """Process uploaded image and return formatted results"""
172
- if image is None:
173
- return "Please upload an image first.", "{}"
174
-
175
- result = processor.process_document(image)
176
-
177
- # Format for display
178
- if result['success']:
179
- display_text = f"""
180
- 📄 **Document Type:** {result['document_type'].upper()}
181
- ⏱️ **Processing Time:** {result['processing_time_seconds']:.2f} seconds
182
-
183
- 🔍 **Extracted Fields:**
184
- """
185
- for field, value in result['extracted_fields'].items():
186
- confidence = result['confidence_scores'].get(field, 0)
187
- display_text += f"• **{field.replace('_', ' ').title()}:** {value} (confidence: {confidence:.2f})\n"
188
 
189
- if not result['extracted_fields']:
190
- display_text += "• No structured fields detected\n"
191
-
192
- display_text += f"\n📝 **Raw Text:**\n{result['raw_text'][:500]}..."
193
-
194
- else:
195
- display_text = f"❌ **Error:** {result['error']}"
196
-
197
- # Return both display text and JSON
198
- json_output = json.dumps(result, indent=2, ensure_ascii=False)
199
- return display_text, json_output
200
 
201
- # API endpoint function
202
- def api_process_image(image):
203
- """API endpoint that returns only JSON"""
204
- if image is None:
205
- return {
206
- 'success': False,
207
- 'error': 'No image provided',
208
- 'timestamp': datetime.now().isoformat()
209
- }
210
-
211
- return processor.process_document(image)
212
 
213
  # Create Gradio interface
214
- def create_interface():
215
- with gr.Blocks(
216
- title="TakeCare - Indonesian Document OCR",
217
- theme=gr.themes.Soft(),
218
- css="""
219
- .main-header {
220
- text-align: center;
221
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
222
- color: white;
223
- padding: 2rem;
224
- border-radius: 1rem;
225
- margin-bottom: 2rem;
226
- }
227
- .upload-section {
228
- border: 2px dashed #667eea;
229
- border-radius: 1rem;
230
- padding: 2rem;
231
- background: #f8f9ff;
232
- }
233
- """
234
- ) as demo:
235
-
236
- # Header
237
- gr.HTML("""
238
- <div class="main-header">
239
- <h1>🏥 TakeCare - Indonesian Document OCR</h1>
240
- <p>Extract data from KTP, BPJS, Kartu Keluarga, and Medical Bills</p>
241
- </div>
242
- """)
243
-
244
  with gr.Row():
245
- with gr.Column(scale=1):
246
- gr.HTML('<div class="upload-section">')
247
- image_input = gr.Image(
248
- label="📷 Upload Document Image",
249
- type="pil",
250
- sources=["upload", "webcam"],
251
- height=400
252
  )
253
- gr.HTML('</div>')
254
-
255
- process_btn = gr.Button(
256
- "🔍 Process Document",
257
- variant="primary",
258
- size="lg"
259
  )
260
-
261
- gr.HTML("""
262
- <div style="margin-top: 1rem; padding: 1rem; background: #e3f2fd; border-radius: 0.5rem;">
263
- <h4>📋 Supported Documents:</h4>
264
- <ul>
265
- <li><strong>KTP</strong> - Kartu Tanda Penduduk</li>
266
- <li><strong>BPJS</strong> - Kartu BPJS Kesehatan</li>
267
- <li><strong>KK</strong> - Kartu Keluarga</li>
268
- <li><strong>Medical Bills</strong> - Hospital/Clinic invoices</li>
269
- </ul>
270
- </div>
271
- """)
272
 
273
- with gr.Column(scale=1):
274
- result_display = gr.Markdown(
275
- label="📊 Processing Results",
276
- value="Upload an image to see results here..."
277
- )
278
-
279
- json_output = gr.Code(
280
- label="📄 JSON Output",
281
- language="json",
282
- value="{}",
283
- interactive=False
284
- )
285
 
286
- # Event handlers
287
  process_btn.click(
288
- fn=process_uploaded_image,
289
- inputs=[image_input],
290
- outputs=[result_display, json_output]
291
- )
292
-
293
- # Auto-process when image is uploaded
294
- image_input.change(
295
- fn=process_uploaded_image,
296
- inputs=[image_input],
297
- outputs=[result_display, json_output]
298
  )
299
-
300
- # API section
301
- gr.HTML("""
302
- <div style="margin-top: 2rem; padding: 1.5rem; background: #f5f5f5; border-radius: 1rem;">
303
- <h3>🔌 API Usage</h3>
304
- <p><strong>Endpoint:</strong> <code>/api/process</code></p>
305
- <p><strong>Method:</strong> POST</p>
306
- <p><strong>Content-Type:</strong> multipart/form-data</p>
307
- <p><strong>Parameter:</strong> <code>image</code> (file upload)</p>
308
-
309
- <h4>Example cURL:</h4>
310
- <pre><code>curl -X POST -F "image=@document.jpg" https://YOUR_SPACE_URL/api/process</code></pre>
311
-
312
- <h4>Example Python:</h4>
313
- <pre><code>import requests
314
-
315
- files = {'image': open('document.jpg', 'rb')}
316
- response = requests.post('https://YOUR_SPACE_URL/api/process', files=files)
317
- result = response.json()</code></pre>
318
- </div>
319
- """)
320
 
321
- return demo
322
-
323
- # Create the interface
324
- demo = create_interface()
325
-
326
- # Add API endpoint
327
- @demo.api(route="/api/process", method="POST")
328
- def api_endpoint(image: gr.File):
329
- """API endpoint for document processing"""
330
- try:
331
- if image is None:
332
- return {
333
- 'success': False,
334
- 'error': 'No image file provided',
335
- 'timestamp': datetime.now().isoformat()
336
- }
337
-
338
- # Load image
339
- pil_image = Image.open(image.name)
340
- result = processor.process_document(pil_image)
341
- return result
342
 
343
- except Exception as e:
344
- return {
345
- 'success': False,
346
- 'error': f'Processing failed: {str(e)}',
347
- 'timestamp': datetime.now().isoformat()
348
- }
349
 
 
350
  if __name__ == "__main__":
351
- demo.launch(
352
- server_name="0.0.0.0",
353
- server_port=7860,
354
- share=True,
355
- show_api=True
356
- )
 
1
  import gradio as gr
 
 
 
2
  import cv2
3
  import numpy as np
4
+ from src.ocr_service import DocumentOCRService
5
+ from src.document_config import HealthcareProcess, DocumentType
6
+ import json
 
7
 
8
+ # Initialize OCR service
9
+ ocr_service = DocumentOCRService()
10
 
11
+ def process_document(image, document_type, process_type):
12
+ """Process document and return results."""
13
+ try:
14
+ # Convert image to numpy array
15
+ image_np = np.array(image)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Convert to BGR for OpenCV
18
+ if len(image_np.shape) == 3 and image_np.shape[2] == 3:
19
+ image_np = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
20
 
21
+ # Process document
22
+ result = ocr_service.process_document(image_np, document_type, process_type)
 
 
 
 
 
 
23
 
24
+ # Format the output
25
+ output = {
26
+ "Extracted Data": result["extracted_data"],
27
+ "Gemini Analysis": result["gemini_analysis"]["analysis"],
28
+ "Validation Result": result["validation_result"]
29
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ return json.dumps(output, indent=2)
32
+ except Exception as e:
33
+ return f"Error processing document: {str(e)}"
 
 
 
 
 
 
 
 
34
 
35
+ def get_requirements(process_type):
36
+ """Get document requirements for a process."""
37
+ try:
38
+ process = HealthcareProcess(process_type)
39
+ requirements = ocr_service.get_process_requirements(process)
40
+ return json.dumps(requirements, indent=2)
41
+ except ValueError as e:
42
+ return f"Error: {str(e)}"
 
 
 
43
 
44
  # Create Gradio interface
45
+ with gr.Blocks(title="TakeCare OCR Service") as demo:
46
+ gr.Markdown("# TakeCare OCR Service")
47
+ gr.Markdown("Upload your healthcare documents for processing and validation.")
48
+
49
+ with gr.Tab("Process Document"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  with gr.Row():
51
+ with gr.Column():
52
+ image_input = gr.Image(type="pil", label="Upload Document")
53
+ document_type = gr.Dropdown(
54
+ choices=[dt.value for dt in DocumentType],
55
+ label="Document Type"
 
 
56
  )
57
+ process_type = gr.Dropdown(
58
+ choices=[pt.value for pt in HealthcareProcess],
59
+ label="Process Type (Optional)"
 
 
 
60
  )
61
+ process_btn = gr.Button("Process Document")
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ with gr.Column():
64
+ output = gr.Textbox(label="Results", lines=20)
 
 
 
 
 
 
 
 
 
 
65
 
 
66
  process_btn.click(
67
+ fn=process_document,
68
+ inputs=[image_input, document_type, process_type],
69
+ outputs=output
 
 
 
 
 
 
 
70
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ with gr.Tab("View Requirements"):
73
+ with gr.Row():
74
+ with gr.Column():
75
+ req_process_type = gr.Dropdown(
76
+ choices=[pt.value for pt in HealthcareProcess],
77
+ label="Select Process Type"
78
+ )
79
+ view_req_btn = gr.Button("View Requirements")
80
+
81
+ with gr.Column():
82
+ requirements_output = gr.Textbox(label="Document Requirements", lines=20)
 
 
 
 
 
 
 
 
 
 
83
 
84
+ view_req_btn.click(
85
+ fn=get_requirements,
86
+ inputs=[req_process_type],
87
+ outputs=requirements_output
88
+ )
 
89
 
90
+ # Launch the app
91
  if __name__ == "__main__":
92
+ demo.launch()