Spaces:
Runtime error
Runtime error
madhurithika22
deploy: add backend codebase and Dockerfile configuration for Hugging Face Spaces
35512c1 | from fastapi import APIRouter, UploadFile, File, HTTPException, Depends | |
| from fastapi.responses import StreamingResponse | |
| from fastapi.concurrency import run_in_threadpool | |
| from core.config import settings | |
| from ml_pipeline.engine import IntelligentDocumentProcessor | |
| from api.dependencies import get_db | |
| from database.repository import DocumentRepository | |
| import aiofiles | |
| import os | |
| import uuid | |
| import io | |
| import pandas as pd | |
| router = APIRouter() | |
| # Load the ML engine directly into the API memory (Bypassing Celery/Redis) | |
| print("Loading ML Models directly into FastAPI...") | |
| ocr_engine = IntelligentDocumentProcessor() | |
| def parse_gemini_error(error_content: str) -> tuple: | |
| try: | |
| import json | |
| err_data = json.loads(error_content) | |
| if "error" in err_data: | |
| err = err_data["error"] | |
| code = err.get("code", 500) | |
| msg = err.get("message", "Unknown Gemini API error") | |
| # Map code/status | |
| if err.get("status") == "RESOURCE_EXHAUSTED" or code == 429: | |
| return 429, f"Gemini API Quota Exceeded: {msg}" | |
| return code if isinstance(code, int) else 500, msg | |
| except Exception: | |
| pass | |
| # Fallback to string search | |
| if "RESOURCE_EXHAUSTED" in error_content or "429" in error_content: | |
| return 429, "Gemini API rate limit or quota exceeded. Please try again later." | |
| if "503" in error_content or "UNAVAILABLE" in error_content: | |
| return 503, "Gemini API service is temporarily unavailable. Please retry shortly." | |
| return 500, f"AI Extraction Pipeline Error: {error_content}" | |
| def merge_page_data(existing_data: dict, new_page_data: dict) -> dict: | |
| merged = existing_data.copy() | |
| for key, val in new_page_data.items(): | |
| if isinstance(val, dict): | |
| if key not in merged or not isinstance(merged[key], dict): | |
| merged[key] = val.copy() | |
| else: | |
| for sub_key, sub_val in val.items(): | |
| if isinstance(sub_val, list): | |
| if sub_key not in merged[key] or not isinstance(merged[key][sub_key], list): | |
| merged[key][sub_key] = sub_val.copy() | |
| else: | |
| merged[key][sub_key].extend(sub_val) | |
| else: | |
| merged[key][sub_key] = sub_val | |
| elif isinstance(val, list): | |
| if key not in merged or not isinstance(merged[key], list): | |
| merged[key] = val.copy() | |
| else: | |
| merged[key].extend(val) | |
| else: | |
| merged[key] = val | |
| return merged | |
| def get_val_global(d: dict, target_key: str): | |
| if not d: | |
| return None | |
| norm_target = "".join(c.lower() for c in target_key if c.isalnum()) | |
| for k, v in d.items(): | |
| if "".join(c.lower() for c in k if c.isalnum()) == norm_target: | |
| return v | |
| return None | |
| def normalize_batch_summary(batch_list: list) -> list: | |
| normalized = [] | |
| if not batch_list or not isinstance(batch_list, list): | |
| return normalized | |
| for row in batch_list: | |
| if not isinstance(row, dict): | |
| continue | |
| normalized.append({ | |
| "p_order": get_val_global(row, "p_order") or get_val_global(row, "p.order") or get_val_global(row, "porder") or get_val_global(row, "order"), | |
| "material_code": get_val_global(row, "material_code") or get_val_global(row, "materialcode") or get_val_global(row, "code"), | |
| "material_description": get_val_global(row, "material_description") or get_val_global(row, "description") or get_val_global(row, "materialdesc") or get_val_global(row, "materialdescription"), | |
| "batch_no": get_val_global(row, "batch_no") or get_val_global(row, "batch") or get_val_global(row, "batchno"), | |
| "t_qty": get_val_global(row, "t_qty") or get_val_global(row, "t.qty") or get_val_global(row, "totalqty") or get_val_global(row, "qty"), | |
| "unit": get_val_global(row, "unit"), | |
| "b_qty": get_val_global(row, "b_qty") or get_val_global(row, "b.qty") or get_val_global(row, "balanceqty") or get_val_global(row, "batchqty") or get_val_global(row, "bqty"), | |
| "t_c_wt": get_val_global(row, "t_c_wt") or get_val_global(row, "t.c.wt") or get_val_global(row, "totalcastwt") or get_val_global(row, "castweight") or get_val_global(row, "tcwt"), | |
| "s_order": get_val_global(row, "s_order") or get_val_global(row, "s.order") or get_val_global(row, "salesorder") or get_val_global(row, "saleorder"), | |
| "s_item": get_val_global(row, "s_item") or get_val_global(row, "s.item") or get_val_global(row, "salesitem"), | |
| "c_code": get_val_global(row, "c_code") or get_val_global(row, "c.code") or get_val_global(row, "customercode"), | |
| "division": get_val_global(row, "division") or get_val_global(row, "div") | |
| }) | |
| return normalized | |
| def map_dynamic_to_queue_page(extracted_data: dict, page_num: int) -> dict: | |
| metadata = extracted_data.get("document_metadata", {}) or {} | |
| product = extracted_data.get("product_details", {}) or {} | |
| pouring = extracted_data.get("pouring_details", {}) or {} | |
| inspection = extracted_data.get("inspection_parameters", {}) or {} | |
| tables = extracted_data.get("tables", {}) or {} | |
| signatures = extracted_data.get("signatures", {}) or {} | |
| get_val = get_val_global | |
| prod_plan = { | |
| "heat_no": get_val(metadata, "heat_no") or get_val(metadata, "cycle_no"), | |
| "planning_date": get_val(metadata, "date"), | |
| "pouring_date": get_val(pouring, "pouring_date") or get_val(pouring, "date"), | |
| "customer": get_val(product, "customer"), | |
| "grade": get_val(product, "grade"), | |
| "casting_weight": get_val(product, "casting_weight"), | |
| "liquid_weight": get_val(product, "liquid_weight"), | |
| "qty": get_val(product, "qty") or get_val(product, "quantity"), | |
| "sample_bulk": get_val(product, "sample_bulk") or get_val(product, "sample_/_bulk"), | |
| "finish_type": get_val(product, "finish_type"), | |
| "pattern_code": get_val(product, "pattern_code"), | |
| "pattern_serial_no": get_val(product, "pattern_serial_no"), | |
| "pattern_type": get_val(product, "pattern_type"), | |
| "drawing_number": get_val(product, "drawing_number") or get_val(product, "drawing_no"), | |
| "part_no": get_val(product, "part_no"), | |
| "pcs_in_box": get_val(product, "pcs_in_box"), | |
| "no_of_core_boxes": get_val(product, "no_of_core_boxes"), | |
| "no_of_cores": get_val(product, "no_of_cores") or get_val(product, "no._of_cores"), | |
| "method_remarks": get_val(product, "method_remarks"), | |
| } | |
| pour_details = { | |
| "pouring_date": get_val(pouring, "pouring_date") or get_val(pouring, "date"), | |
| "pouring_time": get_val(pouring, "pouring_time") or get_val(pouring, "time"), | |
| "pouring_qty": get_val(pouring, "pouring_qty"), | |
| "pouring_sec": get_val(pouring, "pouring_sec") or get_val(pouring, "duration") or get_val(pouring, "pouring_time"), | |
| "tapping_temp": get_val(pouring, "tapping_temp") or get_val(pouring, "tapping_temperature"), | |
| "pouring_temp": get_val(pouring, "pouring_temp") or get_val(pouring, "pouring_temperature"), | |
| "laddle_temp": get_val(pouring, "laddle_temp") or get_val(pouring, "ladle_temp"), | |
| "pouring_weight": get_val(pouring, "pouring_weight") or get_val(product, "liquid_weight"), | |
| "core_making": get_val(pouring, "core_making"), | |
| } | |
| qa_params = { | |
| "hardness_mould": get_val(inspection, "hardness_range_mould") or get_val(inspection, "mould_hardness_range") or get_val(inspection, "hardness_range_mould_70_to_85"), | |
| "hardness_core": get_val(inspection, "hardness_range_core") or get_val(inspection, "core_hardness_range") or get_val(inspection, "hardness_range_core_65_to_85") or get_val(inspection, "hardness/range(core)"), | |
| "coating_baume_value": get_val(inspection, "coating_baume_value") or get_val(inspection, "coating_baume_value_range") or get_val(inspection, "coating_baume_value_range_53_to_65"), | |
| "core_oven_baking_on_time": get_val(inspection, "core_oven_baking_on_time"), | |
| "core_oven_baking_off_time": get_val(inspection, "core_oven_baking_off_time"), | |
| "core_oven_preheating_temp": get_val(inspection, "core_oven_preheating_temp"), | |
| "no_of_cores": get_val(inspection, "no_of_cores"), | |
| "mould_coating": get_val(inspection, "mould_coating"), | |
| "core_coating": get_val(inspection, "core_coating"), | |
| "lettering_checking": get_val(inspection, "lettering_checking"), | |
| "mould_core_visual_checking": get_val(inspection, "mould_core_visual_checking") or get_val(inspection, "mould_&_core_visual_checking"), | |
| "mould_core_coating_application": get_val(inspection, "mould_core_coating_application") or get_val(inspection, "mould_&_core_coating_application"), | |
| "core_setting_wall_thickness": get_val(inspection, "core_setting_wall_thickness"), | |
| "mould_core_preheating": get_val(inspection, "mould_core_preheating") or get_val(inspection, "mould_&_core_preheating"), | |
| "templates_checking": get_val(inspection, "templates_checking"), | |
| "core_setting_inspector": get_val(inspection, "core_setting_inspector") or get_val(inspection, "core_setting"), | |
| "closing_inspector": get_val(inspection, "closing_inspector") or get_val(inspection, "closing"), | |
| "pouring_inspector": get_val(inspection, "pouring_inspector") or get_val(inspection, "pouring"), | |
| } | |
| sleeve_list = [] | |
| for s in tables.get("sleeves", []) or []: | |
| sleeve_list.append({ | |
| "sle_code": get_val(s, "code"), | |
| "sle_name": get_val(s, "name"), | |
| "slv_qty": get_val(s, "qty") | |
| }) | |
| consumable_list = [] | |
| for c in tables.get("consumables", []) or []: | |
| consumable_list.append({ | |
| "item": get_val(c, "item"), | |
| "quantity": get_val(c, "qty") or get_val(c, "quantity") | |
| }) | |
| headers = { | |
| "form_id": get_val(metadata, "form_id"), | |
| "heat_no": get_val(metadata, "heat_no") or get_val(metadata, "cycle_no"), | |
| "planning_date": get_val(metadata, "date"), | |
| "pouring_date_header": get_val(pouring, "pouring_date") or get_val(pouring, "date"), | |
| } | |
| return { | |
| "page_number": page_num, | |
| "document_headers": headers, | |
| "production_plan": prod_plan, | |
| "pouring_details": pour_details, | |
| "qa_parameters": qa_params, | |
| "bottom_signatures": signatures, | |
| "sleeve_table": sleeve_list, | |
| "handwritten_consumables_list": consumable_list | |
| } | |
| async def upload_and_process_document( | |
| file: UploadFile = File(None), | |
| filename: str = None, | |
| page: int = 0, | |
| task_id: str = None, | |
| db = Depends(get_db) | |
| ): | |
| """ | |
| Accepts an industrial scan page-by-page. For initial page (page=0), accepts a file upload. | |
| For subsequent pages, accepts filename to reuse the saved document path. | |
| Integrates results incrementally into MongoDB. | |
| """ | |
| if not file and not filename: | |
| raise HTTPException(status_code=400, detail="Either file or filename must be provided.") | |
| if file: | |
| allowed_types = ["image/jpeg", "image/png", "application/pdf"] | |
| file_extension = file.filename.split(".")[-1].lower() | |
| # Verify content type or extension matches allowed files (more robust for browser variations) | |
| is_allowed = ( | |
| file.content_type in allowed_types or | |
| file_extension in ["pdf", "jpg", "jpeg", "png"] | |
| ) | |
| if not is_allowed: | |
| raise HTTPException(status_code=400, detail="Unsupported file type. Use JPG, PNG, or PDF.") | |
| unique_filename = f"{uuid.uuid4().hex}.{file_extension}" | |
| file_path = os.path.join(settings.UPLOAD_DIR, unique_filename) | |
| # Save file | |
| async with aiofiles.open(file_path, 'wb') as out_file: | |
| content = await file.read() | |
| await out_file.write(content) | |
| filename_used = unique_filename | |
| else: | |
| file_path = os.path.join(settings.UPLOAD_DIR, filename) | |
| if not os.path.exists(file_path): | |
| raise HTTPException(status_code=404, detail=f"Saved file {filename} not found on server.") | |
| filename_used = filename | |
| try: | |
| # Process the specific page | |
| result_payload = await run_in_threadpool(ocr_engine.process_document, file_path, page_num=page) | |
| # Enhanced debugging log | |
| print(f"DEBUG - Extracted page results payload: {result_payload}") | |
| if isinstance(result_payload, dict) and "error" in result_payload: | |
| error_msg = result_payload['error'] | |
| status, cleaned_msg = parse_gemini_error(error_msg) | |
| raise HTTPException( | |
| status_code=status, | |
| detail=cleaned_msg | |
| ) | |
| extracted_data = result_payload["extracted_data"] | |
| total_pages = result_payload["total_pages"] | |
| # Normalize/map to structured queue_pages schema | |
| if "queue_pages" not in extracted_data: | |
| page_data = map_dynamic_to_queue_page(extracted_data, page_num=page) | |
| raw_batch = extracted_data.get("tables", {}).get("batch_summary", []) or [] | |
| new_extracted_data = { | |
| "queue_pages": [page_data], | |
| "batch_summary": normalize_batch_summary(raw_batch) | |
| } | |
| else: | |
| new_extracted_data = extracted_data | |
| if "batch_summary" in new_extracted_data: | |
| new_extracted_data["batch_summary"] = normalize_batch_summary(new_extracted_data["batch_summary"]) | |
| elif "tables" in new_extracted_data and "batch_summary" in new_extracted_data["tables"]: | |
| new_extracted_data["batch_summary"] = normalize_batch_summary(new_extracted_data["tables"]["batch_summary"]) | |
| # Save to database (MongoDB) and merge if task_id is provided | |
| repo = DocumentRepository(db) | |
| if task_id: | |
| existing_doc = await repo.get_document(task_id) | |
| if existing_doc: | |
| existing_extracted = existing_doc.get("extracted_data", {}) or {} | |
| if "queue_pages" in existing_extracted and "queue_pages" in new_extracted_data: | |
| existing_pages = existing_extracted.get("queue_pages", []) | |
| new_pages = new_extracted_data.get("queue_pages", []) | |
| # Remove any existing page with the same page number and append the new page | |
| existing_pages = [p for p in existing_pages if p.get("page_number") != page] | |
| existing_pages.extend(new_pages) | |
| existing_pages.sort(key=lambda p: p.get("page_number", 0)) | |
| # Merge batch summary tables (normalizing existing too to cleanse any old schemas) | |
| existing_batch = normalize_batch_summary(existing_extracted.get("batch_summary", []) or []) | |
| new_batch = new_extracted_data.get("batch_summary", []) or [] | |
| existing_batch.extend(new_batch) | |
| # Deduplicate batch summary entries | |
| seen_batch = set() | |
| unique_batch = [] | |
| for row in existing_batch: | |
| row_key = (row.get("material_code"), row.get("batch_no"), row.get("t_qty")) | |
| if row_key not in seen_batch: | |
| seen_batch.add(row_key) | |
| unique_batch.append(row) | |
| accumulated_data = { | |
| "queue_pages": existing_pages, | |
| "batch_summary": unique_batch | |
| } | |
| else: | |
| accumulated_data = merge_page_data(existing_extracted, new_extracted_data) | |
| await repo.save_document(task_id, accumulated_data, filename=filename_used) | |
| else: | |
| await repo.save_document(task_id, new_extracted_data, filename=filename_used) | |
| accumulated_data = new_extracted_data | |
| else: | |
| task_id = uuid.uuid4().hex | |
| await repo.save_document(task_id, new_extracted_data, filename=filename_used) | |
| accumulated_data = new_extracted_data | |
| return { | |
| "message": "Page processed successfully", | |
| "filename": filename_used, | |
| "task_id": task_id, | |
| "data": accumulated_data, | |
| "current_page": page, | |
| "total_pages": total_pages, | |
| "has_next_page": page < total_pages - 1 | |
| } | |
| except HTTPException as he: | |
| raise he | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Processing failed inside route: {str(e)}") | |
| async def get_all_processed_documents(db = Depends(get_db)): | |
| """ | |
| Retrieves all processed document records from the database. | |
| """ | |
| try: | |
| repo = DocumentRepository(db) | |
| records = await repo.get_all_documents() | |
| for doc in records: | |
| if "extracted_data" in doc and doc["extracted_data"]: | |
| data = doc["extracted_data"] | |
| if "batch_summary" in data: | |
| data["batch_summary"] = normalize_batch_summary(data["batch_summary"]) | |
| return records | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to retrieve records: {str(e)}") | |
| async def export_all_data_to_excel(db = Depends(get_db)): | |
| """ | |
| Aggregates all processed document records and converts them to a multi-sheet Excel file. | |
| Sheet 1: Queue Data (Pages 1-5) | |
| Sheet 2: Batch Summary (Page 6) | |
| """ | |
| try: | |
| repo = DocumentRepository(db) | |
| documents = await repo.get_all_documents() | |
| queue_rows = [] | |
| batch_rows = [] | |
| # Parse the JSON structure into flat rows for Excel | |
| for doc in documents: | |
| data = doc.get("extracted_data", {}) | |
| # 1. Flatten Queue Pages (Original / 6-Page schemas) | |
| if "queue_pages" in data: | |
| for page in data.get("queue_pages", []): | |
| prod = page.get("production_plan", {}) or {} | |
| qa = page.get("qa_parameters", {}) or {} | |
| pour = page.get("pouring_details", {}) or {} | |
| queue_rows.append({ | |
| "Task ID": doc.get("task_id", "N/A"), | |
| "Page No": page.get("page_number", ""), | |
| "Heat No": prod.get("heat_no", ""), | |
| "Planning Date": prod.get("planning_date", ""), | |
| "Pouring Date": prod.get("pouring_date", ""), | |
| "Customer": prod.get("customer", ""), | |
| "Grade": prod.get("grade", ""), | |
| "Casting Wt": prod.get("casting_weight", ""), | |
| "Mould Hardness": qa.get("hardness_mould", ""), | |
| "Core Hardness": qa.get("hardness_core", ""), | |
| "Pouring Time": pour.get("pouring_time", ""), | |
| "Tapping Temp": pour.get("tapping_temp", ""), | |
| "Pouring Temp": pour.get("pouring_temp", ""), | |
| "Laddle Temp": pour.get("laddle_temp", ""), | |
| "Pouring Wt": pour.get("pouring_weight", "") | |
| }) | |
| # 2. Flatten Dynamic Schema | |
| elif "document_metadata" in data or "pouring_details" in data: | |
| metadata = data.get("document_metadata", {}) or {} | |
| prod = data.get("product_details", {}) or {} | |
| pour = data.get("pouring_details", {}) or {} | |
| inspect = data.get("inspection_parameters", {}) or {} | |
| temps_str = pour.get("pouring_temperature", "") or "" | |
| temps = [t.strip() for t in temps_str.split(',')] if temps_str else [""] | |
| durations_str = pour.get("duration", "") or "" | |
| durations = [d.strip() for d in durations_str.split(',')] if durations_str else [""] | |
| count = max(len(temps), len(durations), 1) | |
| for i in range(count): | |
| p_temp = temps[i] if i < len(temps) else "" | |
| p_dur = durations[i] if i < len(durations) else "" | |
| queue_rows.append({ | |
| "Task ID": doc.get("task_id", "N/A"), | |
| "Page No": f"Pour {i+1}", | |
| "Heat No": metadata.get("heat_no", ""), | |
| "Planning Date": metadata.get("date", ""), | |
| "Pouring Date": pour.get("date", ""), | |
| "Customer": prod.get("customer", ""), | |
| "Grade": prod.get("grade", ""), | |
| "Casting Wt": prod.get("casting_weight", ""), | |
| "Mould Hardness": inspect.get("mould_hardness_range", ""), | |
| "Core Hardness": inspect.get("core_hardness_range", ""), | |
| "Pouring Time": p_dur, | |
| "Tapping Temp": pour.get("tapping_temperature", ""), | |
| "Pouring Temp": p_temp, | |
| "Ladle Temp": pour.get("laddle_temp", ""), | |
| "Pouring Wt": pour.get("pouring_weight", "") | |
| }) | |
| # 3. Flatten Batch Summary Table (Original / 6-Page schemas) | |
| if "batch_summary" in data: | |
| normalized_batch = normalize_batch_summary(data.get("batch_summary", [])) | |
| for row in normalized_batch: | |
| batch_rows.append({ | |
| "Task ID": doc.get("task_id", "N/A"), | |
| "P.Order": row.get("p_order", ""), | |
| "Material Code": row.get("material_code", ""), | |
| "Material Description": row.get("material_description", ""), | |
| "Batch No": row.get("batch_no", ""), | |
| "Total Qty": row.get("t_qty", ""), | |
| "Unit": row.get("unit", ""), | |
| "B.Qty": row.get("b_qty", ""), | |
| "T.C.Wt": row.get("t_c_wt", ""), | |
| "S.Order": row.get("s_order", ""), | |
| "S.Item": row.get("s_item", ""), | |
| "C.Code": row.get("c_code", ""), | |
| "Division": row.get("division", "") | |
| }) | |
| # 4. Flatten Batch Summary Table (Dynamic Schema) | |
| elif "tables" in data and "batch_summary" in data.get("tables", {}): | |
| for row in data.get("tables", {}).get("batch_summary", []): | |
| normalized_row = normalize_batch_summary([row])[0] if normalize_batch_summary([row]) else {} | |
| batch_rows.append({ | |
| "Task ID": doc.get("task_id", "N/A"), | |
| "P.Order": normalized_row.get("p_order", ""), | |
| "Material Code": normalized_row.get("material_code", ""), | |
| "Material Description": normalized_row.get("material_description", ""), | |
| "Batch No": normalized_row.get("batch_no", ""), | |
| "Total Qty": normalized_row.get("t_qty", ""), | |
| "Unit": normalized_row.get("unit", ""), | |
| "B.Qty": normalized_row.get("b_qty", ""), | |
| "T.C.Wt": normalized_row.get("t_c_wt", ""), | |
| "S.Order": normalized_row.get("s_order", ""), | |
| "S.Item": normalized_row.get("s_item", ""), | |
| "C.Code": normalized_row.get("c_code", ""), | |
| "Division": normalized_row.get("division", "") | |
| }) | |
| # Convert to Pandas DataFrames | |
| df_queue = pd.DataFrame(queue_rows) if queue_rows else pd.DataFrame(columns=["Heat No", "Pouring Date", "Customer"]) | |
| df_batch = pd.DataFrame(batch_rows) if batch_rows else pd.DataFrame(columns=["Material Code", "Batch No", "Total Qty"]) | |
| # Write to memory buffer | |
| buffer = io.BytesIO() | |
| with pd.ExcelWriter(buffer, engine='openpyxl') as writer: | |
| df_queue.to_excel(writer, index=False, sheet_name='Production Queue (P1-P5)') | |
| df_batch.to_excel(writer, index=False, sheet_name='Batch Summary (P6)') | |
| buffer.seek(0) | |
| return StreamingResponse( | |
| buffer, | |
| media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| headers={"Content-Disposition": "attachment; filename=manufacturing_records.xlsx"} | |
| ) | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to export data: {str(e)}") | |
| async def get_processing_status(task_id: str): | |
| return {"task_id": task_id, "status": "SYNC_MODE_ACTIVE", "message": "Redis is disabled. Check the main /process route for output."} |