| """ |
| Tesseract++ Web Application |
| Academic-grade web interface for floorplan to graph conversion |
| """ |
|
|
| import os |
| import sys |
| import json |
| import time |
| import uuid |
| import asyncio |
| import tempfile |
| import shutil |
| from pathlib import Path |
| from typing import Optional, Dict, Any, List |
| from datetime import datetime, timedelta |
|
|
| from fastapi import FastAPI, File, UploadFile, HTTPException, Request |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.responses import JSONResponse, FileResponse |
| from pydantic import BaseModel |
| import uvicorn |
|
|
| |
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
| sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "utils")) |
|
|
| |
| from utils.app_utils.api.models import ProcessingResponse, GraphVisualization, ExampleImage |
| from utils.app_utils.api.processing import ProcessingPipeline, get_progress |
| from utils.app_utils.visualization.graph_converter import convert_to_cytoscape |
|
|
| |
| app = FastAPI( |
| title="Tesseract++ Floorplan Analyzer", |
| description="Convert architectural floorplans to navigable graphs", |
| version="1.0.0" |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| UPLOAD_LIMIT_MB = 10 |
| PROCESSING_TIMEOUT = 180 |
|
|
| |
| _BASE_DIR = Path(__file__).parent |
| MODEL_WEIGHTS_DIR = _BASE_DIR / "Model_weights" |
| INPUT_IMAGES_DIR = _BASE_DIR / "Input_Images" |
| RESULTS_DIR = _BASE_DIR / "Results" |
|
|
| |
| CURATED_EXAMPLES = [ |
| "FF part 1upE.png", |
| "FF part 2up.png", |
| "FF part 3upE.png", |
| ] |
|
|
| |
| active_sessions: Dict[str, Dict[str, Any]] = {} |
|
|
| |
| pipeline = None |
|
|
| class HealthCheck(BaseModel): |
| status: str |
| models_loaded: bool |
| example_images: int |
| message: str |
|
|
| @app.on_event("startup") |
| async def startup_event(): |
| """Initialize application on startup""" |
| global pipeline |
|
|
| print("=" * 50) |
| print("Tesseract++ Web Application Starting...") |
| print("=" * 50) |
|
|
| |
| model_checks = { |
| "CRAFT Text Detector": "craft_mlt_25k.pth", |
| "Text Interpreter": "None-VGG-BiLSTM-CTC.pth", |
| "Door Detector": "door_mdl_32.pth" |
| } |
|
|
| missing_models = [] |
| for model_name, weight_file in model_checks.items(): |
| weight_path = MODEL_WEIGHTS_DIR / weight_file |
| if not weight_path.exists(): |
| missing_models.append(f"{model_name} ({weight_file})") |
| else: |
| print(f" {model_name} weights found") |
|
|
| if missing_models: |
| error_msg = f"Missing model weights in {MODEL_WEIGHTS_DIR}:\n" + "\n".join(missing_models) |
| print(f"ERROR: {error_msg}") |
| raise RuntimeError(error_msg) |
|
|
| |
| try: |
| pipeline = ProcessingPipeline() |
| print(" Processing pipeline initialized") |
| except Exception as e: |
| print(f"ERROR: Failed to initialize pipeline: {e}") |
| raise |
|
|
| |
| found = sum(1 for name in CURATED_EXAMPLES if (INPUT_IMAGES_DIR / name).exists()) |
| print(f" Found {found}/{len(CURATED_EXAMPLES)} curated example images") |
|
|
| print("=" * 50) |
| print("Application ready!") |
| print("=" * 50) |
|
|
| @app.on_event("shutdown") |
| async def shutdown_event(): |
| """Cleanup on shutdown""" |
| for session_id, session_data in active_sessions.items(): |
| if "temp_file" in session_data and session_data["temp_file"] and os.path.exists(session_data["temp_file"]): |
| os.remove(session_data["temp_file"]) |
| active_sessions.clear() |
|
|
| @app.get("/health", response_model=HealthCheck) |
| async def health_check(): |
| """Health check endpoint""" |
| models_loaded = pipeline is not None |
| found = sum(1 for name in CURATED_EXAMPLES if (INPUT_IMAGES_DIR / name).exists()) |
|
|
| return HealthCheck( |
| status="healthy" if models_loaded else "unhealthy", |
| models_loaded=models_loaded, |
| example_images=found, |
| message="System ready for processing" if models_loaded else "Models not loaded" |
| ) |
|
|
| @app.get("/api/examples", response_model=List[ExampleImage]) |
| async def get_example_images(): |
| """Get list of curated example images""" |
| examples = [] |
|
|
| for img_name in CURATED_EXAMPLES: |
| img_path = INPUT_IMAGES_DIR / img_name |
| if not img_path.exists(): |
| continue |
|
|
| stat = img_path.stat() |
| has_cached = pipeline.has_cached_result(img_name) if pipeline else False |
|
|
| examples.append(ExampleImage( |
| name=img_name, |
| display_name=img_path.stem.replace("_", " "), |
| size_kb=round(stat.st_size / 1024, 1), |
| has_cached_result=has_cached |
| )) |
|
|
| return examples |
|
|
| @app.get("/api/example-image/{image_name}") |
| async def get_example_image(image_name: str): |
| """Serve example image thumbnail""" |
| image_path = INPUT_IMAGES_DIR / image_name |
| if not image_path.exists() or not image_path.is_file(): |
| raise HTTPException(status_code=404, detail="Example image not found") |
|
|
| return FileResponse(image_path, media_type="image/png") |
|
|
| @app.get("/api/floorplan-image/{image_name}") |
| async def get_floorplan_image(image_name: str): |
| """Serve original floorplan image for background overlay""" |
| |
| image_path = INPUT_IMAGES_DIR / image_name |
| if image_path.exists() and image_path.is_file(): |
| return FileResponse(image_path, media_type="image/png") |
|
|
| |
| if pipeline: |
| temp_path = pipeline.temp_dir / image_name |
| if temp_path.exists() and temp_path.is_file(): |
| return FileResponse(temp_path, media_type="image/png") |
|
|
| raise HTTPException(status_code=404, detail="Floorplan image not found") |
|
|
| @app.get("/api/cached-result/{image_name}") |
| async def get_cached_result(image_name: str): |
| """Get pre-computed result for a cached example image""" |
| if not pipeline: |
| raise HTTPException(status_code=503, detail="Pipeline not initialized") |
|
|
| cached = pipeline.get_cached_result(image_name) |
| if cached is None: |
| raise HTTPException(status_code=404, detail="No cached result for this image") |
|
|
| cytoscape_data = convert_to_cytoscape(cached["graph_json"]) |
| pre_pruning_cytoscape = ( |
| convert_to_cytoscape(cached["pre_pruning_graph_json"]) |
| if cached.get("pre_pruning_graph_json") else None |
| ) |
|
|
| return ProcessingResponse( |
| session_id=str(uuid.uuid4()), |
| status="success", |
| image_name=image_name, |
| processing_time=0.0, |
| graph_data=cytoscape_data, |
| pre_pruning_graph_data=pre_pruning_cytoscape, |
| statistics={ |
| "total_nodes": cached["stats"]["total_nodes"], |
| "total_edges": cached["stats"]["total_edges"], |
| "node_types": cached["stats"]["node_types"], |
| "pruning_reduction": cached["stats"].get("pruning_reduction", 0) |
| }, |
| message=f"Loaded cached result for {image_name}" |
| ) |
|
|
| @app.get("/api/progress/{image_name}") |
| async def get_processing_progress(image_name: str): |
| """Get current processing stage for an image""" |
| stage = get_progress(image_name) |
| return {"stage": stage} |
|
|
| @app.post("/api/process") |
| async def process_image( |
| request: Request, |
| file: Optional[UploadFile] = File(None), |
| example: Optional[str] = None |
| ): |
| """Process uploaded image or example image""" |
|
|
| |
| session_id = str(uuid.uuid4()) |
|
|
| |
| client_ip = request.client.host |
| for sid, data in active_sessions.items(): |
| if data.get("client_ip") == client_ip and data.get("status") == "processing": |
| raise HTTPException( |
| status_code=429, |
| detail="Already processing an image. Please wait for completion." |
| ) |
|
|
| try: |
| |
| if file and file.filename: |
| |
| contents = await file.read() |
| size_mb = len(contents) / (1024 * 1024) |
| if size_mb > UPLOAD_LIMIT_MB: |
| raise HTTPException( |
| status_code=413, |
| detail=f"File too large. Maximum size is {UPLOAD_LIMIT_MB}MB" |
| ) |
|
|
| |
| temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") |
| temp_file.write(contents) |
| temp_file.close() |
|
|
| image_path = temp_file.name |
| image_name = file.filename |
| is_example = False |
|
|
| elif example: |
| |
| if pipeline: |
| cached = pipeline.get_cached_result(example) |
| if cached is not None: |
| cytoscape_data = convert_to_cytoscape(cached["graph_json"]) |
| pre_pruning_cytoscape = ( |
| convert_to_cytoscape(cached["pre_pruning_graph_json"]) |
| if cached.get("pre_pruning_graph_json") else None |
| ) |
| return ProcessingResponse( |
| session_id=session_id, |
| status="success", |
| image_name=example, |
| processing_time=0.0, |
| graph_data=cytoscape_data, |
| pre_pruning_graph_data=pre_pruning_cytoscape, |
| statistics={ |
| "total_nodes": cached["stats"]["total_nodes"], |
| "total_edges": cached["stats"]["total_edges"], |
| "node_types": cached["stats"]["node_types"], |
| "pruning_reduction": cached["stats"].get("pruning_reduction", 0) |
| }, |
| message=f"Loaded cached result for {example}" |
| ) |
|
|
| |
| image_path = str(INPUT_IMAGES_DIR / example) |
| if not os.path.exists(image_path): |
| raise HTTPException(status_code=404, detail="Example image not found") |
|
|
| image_name = example |
| is_example = True |
|
|
| else: |
| raise HTTPException(status_code=400, detail="No image provided") |
|
|
| |
| active_sessions[session_id] = { |
| "client_ip": client_ip, |
| "status": "processing", |
| "start_time": time.time(), |
| "image_name": image_name, |
| "temp_file": image_path if not is_example else None |
| } |
|
|
| |
| start_time = time.time() |
|
|
| try: |
| |
| result = await asyncio.to_thread( |
| pipeline.process_image, |
| image_path, |
| image_name, |
| timeout=PROCESSING_TIMEOUT, |
| progress_key=image_name |
| ) |
|
|
| processing_time = time.time() - start_time |
|
|
| |
| cytoscape_data = convert_to_cytoscape(result["graph_json"]) |
| pre_pruning_cytoscape = ( |
| convert_to_cytoscape(result["pre_pruning_graph_json"]) |
| if result.get("pre_pruning_graph_json") else None |
| ) |
|
|
| |
| response = ProcessingResponse( |
| session_id=session_id, |
| status="success", |
| image_name=image_name, |
| processing_time=processing_time, |
| graph_data=cytoscape_data, |
| pre_pruning_graph_data=pre_pruning_cytoscape, |
| statistics={ |
| "total_nodes": result["stats"]["total_nodes"], |
| "total_edges": result["stats"]["total_edges"], |
| "node_types": result["stats"]["node_types"], |
| "pruning_reduction": result["stats"].get("pruning_reduction", 0) |
| }, |
| message=f"Successfully processed {image_name}" |
| ) |
|
|
| |
| active_sessions[session_id]["status"] = "completed" |
| active_sessions[session_id]["result"] = response.dict() |
|
|
| |
| if not is_example and os.path.exists(image_path): |
| if pipeline: |
| overlay_path = pipeline.temp_dir / image_name |
| shutil.copy2(image_path, str(overlay_path)) |
| os.remove(image_path) |
|
|
| return response |
|
|
| except TimeoutError: |
| raise HTTPException( |
| status_code=504, |
| detail=f"Processing timeout exceeded ({PROCESSING_TIMEOUT}s)" |
| ) |
| except Exception as e: |
| raise HTTPException( |
| status_code=500, |
| detail=f"Processing error: {str(e)}" |
| ) |
|
|
| finally: |
| |
| if session_id in active_sessions: |
| active_sessions[session_id]["status"] = "completed" |
|
|
| @app.get("/api/session/{session_id}") |
| async def get_session_result(session_id: str): |
| """Get result for a session""" |
| if session_id not in active_sessions: |
| raise HTTPException(status_code=404, detail="Session not found") |
|
|
| session = active_sessions[session_id] |
| if session["status"] == "processing": |
| return {"status": "processing", "message": "Still processing..."} |
|
|
| return session.get("result", {"status": "error", "message": "No result available"}) |
|
|
| @app.delete("/api/session/{session_id}") |
| async def clear_session(session_id: str): |
| """Clear a session and its data""" |
| if session_id in active_sessions: |
| session = active_sessions[session_id] |
| if "temp_file" in session and session["temp_file"] and os.path.exists(session["temp_file"]): |
| os.remove(session["temp_file"]) |
| del active_sessions[session_id] |
| return {"message": "Session cleared"} |
|
|
| return {"message": "Session not found"} |
|
|
| |
| frontend_dir = Path(__file__).parent / "utils" / "app_utils" / "frontend" / "dist" |
| if frontend_dir.exists(): |
| app.mount("/", StaticFiles(directory=str(frontend_dir), html=True), name="frontend") |
| else: |
| @app.get("/") |
| async def root(): |
| return { |
| "message": "Tesseract++ API is running. Frontend not built yet.", |
| "docs": "/docs", |
| "health": "/health" |
| } |
|
|
| if __name__ == "__main__": |
| uvicorn.run( |
| "app:app", |
| host="0.0.0.0", |
| port=int(os.environ.get("PORT", 7860)), |
| reload=False, |
| log_level="info" |
| ) |
|
|