Spaces:
Sleeping
Sleeping
| """Gradio app: OCR documents into structured notification/data records. | |
| Upload scanned PDFs or images (construction reports, compliance | |
| certificates, invoices, etc.), scan the extracted text for domain | |
| "notification" keywords, pull out structured fields (PO/SAP/invoice | |
| numbers, dates, amounts, cert codes), and export everything to CSV. | |
| """ | |
| import os | |
| import tempfile | |
| from datetime import datetime | |
| import gradio as gr | |
| import pandas as pd | |
| import extractors | |
| import ocr_engine | |
| CATEGORY_CHOICES = ["construction", "compliance", "finance", "general"] | |
| COLUMNS = [ | |
| "file_name", "page", "text_source", "ocr_confidence", | |
| "notification_flags", "notification_context", | |
| "po_numbers", "sap_doc_numbers", "invoice_numbers", "cert_codes", | |
| "dates", "amounts", "percentages", | |
| ] | |
| def _file_path(f): | |
| return f if isinstance(f, str) else f.name | |
| def process_files(files, categories, custom_keywords_str, only_matches): | |
| if not files: | |
| return "Upload at least one PDF or image file.", pd.DataFrame(columns=COLUMNS), None, "" | |
| custom_keywords = None | |
| if custom_keywords_str and custom_keywords_str.strip(): | |
| custom_keywords = [k.strip() for k in custom_keywords_str.split(",") if k.strip()] | |
| rows = [] | |
| full_text_log = [] | |
| warnings = [] | |
| total_pages = 0 | |
| for f in files: | |
| path = _file_path(f) | |
| file_name = os.path.basename(path) | |
| try: | |
| pages = ocr_engine.extract_pages(path) | |
| except Exception as exc: | |
| warnings.append(f"{file_name}: {exc}") | |
| continue | |
| for page_data in pages: | |
| total_pages += 1 | |
| text = page_data["text"] | |
| notifications = extractors.find_notifications(text, categories, custom_keywords) | |
| fields = extractors.extract_fields(text) | |
| has_content = bool(notifications) or any(fields.values()) | |
| full_text_log.append( | |
| f"--- {file_name} | page {page_data['page']} | {page_data['source']} ---\n{text}\n" | |
| ) | |
| if only_matches and not has_content: | |
| continue | |
| contexts = [] | |
| for n in notifications: | |
| if n["context"] not in contexts: | |
| contexts.append(n["context"]) | |
| rows.append({ | |
| "file_name": file_name, | |
| "page": page_data["page"], | |
| "text_source": page_data["source"], | |
| "ocr_confidence": page_data["ocr_confidence"], | |
| "notification_flags": "; ".join( | |
| sorted({f"[{n['category']}] {n['keyword']}" for n in notifications}) | |
| ), | |
| "notification_context": " | ".join(contexts[:5]), | |
| "po_numbers": "; ".join(fields["po_numbers"]), | |
| "sap_doc_numbers": "; ".join(fields["sap_doc_numbers"]), | |
| "invoice_numbers": "; ".join(fields["invoice_numbers"]), | |
| "cert_codes": "; ".join(fields["cert_codes"]), | |
| "dates": "; ".join(fields["dates"]), | |
| "amounts": "; ".join(fields["amounts"]), | |
| "percentages": "; ".join(fields["percentages"]), | |
| }) | |
| df = pd.DataFrame(rows, columns=COLUMNS) | |
| csv_path = None | |
| if not df.empty: | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| csv_path = os.path.join(tempfile.gettempdir(), f"ocr_extract_{timestamp}.csv") | |
| df.to_csv(csv_path, index=False) | |
| status_lines = [ | |
| f"Processed {len(files)} file(s), {total_pages} page(s) total.", | |
| f"{len(df)} row(s) exported" + (" (matches only)." if only_matches else "."), | |
| ] | |
| if warnings: | |
| status_lines.append("Warnings: " + "; ".join(warnings)) | |
| status = "\n".join(status_lines) | |
| return status, df, csv_path, "\n".join(full_text_log) | |
| with gr.Blocks(title="OCR Notification & Data Extractor") as demo: | |
| gr.Markdown( | |
| "# OCR Notification & Data Extractor\n" | |
| "Upload scanned PDFs or images (construction reports, compliance " | |
| "certificates, invoices, SAP printouts, etc.). The app OCRs each " | |
| "page, flags notification-worthy keywords per domain, extracts " | |
| "structured fields (PO/SAP/invoice numbers, dates, amounts, cert " | |
| "codes), and exports everything to CSV." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| files_input = gr.Files( | |
| label="PDF or image files", | |
| file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"], | |
| ) | |
| categories_input = gr.CheckboxGroup( | |
| choices=CATEGORY_CHOICES, | |
| value=CATEGORY_CHOICES, | |
| label="Notification categories", | |
| ) | |
| custom_keywords_input = gr.Textbox( | |
| label="Custom keywords (comma-separated, optional)", | |
| placeholder="e.g. re-inspection, warranty claim, hold payment", | |
| ) | |
| only_matches_input = gr.Checkbox( | |
| value=True, | |
| label="Only export pages with a notification hit or extracted field", | |
| ) | |
| run_button = gr.Button("Run OCR & Extract", variant="primary") | |
| with gr.Column(scale=2): | |
| status_output = gr.Textbox(label="Status", lines=3, interactive=False) | |
| table_output = gr.Dataframe(label="Extracted records", headers=COLUMNS, wrap=True) | |
| csv_output = gr.File(label="Download CSV") | |
| with gr.Accordion("Full OCR text (debug)", open=False): | |
| text_output = gr.Textbox(label="Raw extracted text", lines=20) | |
| run_button.click( | |
| fn=process_files, | |
| inputs=[files_input, categories_input, custom_keywords_input, only_matches_input], | |
| outputs=[status_output, table_output, csv_output, text_output], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |