CoolFace
Apppublic

fmard/ocr-api

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
ocr_service.py229 linesDownload Raw Back to python
1"""2OCR Service — PaddleOCR wrapper as HTTP API.3 4This service runs as a separate Python process and is called5by the Go orchestrator's OCR agent via HTTP.6 7Endpoints:8    POST /ocr  — Extract text from image9    GET  /health — Health check10"""11 12import io13import logging14import os15import time16 17import numpy as np18from flask import Flask, request, jsonify19from PIL import Image20from paddleocr import PaddleOCR21 22from preprocessor import preprocess23 24# Setup logging25logging.basicConfig(26    level=logging.INFO,27    format="%(asctime)s [%(levelname)s] %(message)s"28)29logger = logging.getLogger(__name__)30 31app = Flask(__name__)32 33# Multiple OCR engines for retry strategy34ocr_engines = {}35 36 37def get_ocr_engine(config_name="default"):38    """Lazy-load OCR engine with specific config."""39    global ocr_engines40    if config_name not in ocr_engines:41        logger.info(f"Loading PaddleOCR model config '{config_name}'...")42 43        configs = {44            "default": {45                "use_angle_cls": True,46                "lang": "en",47                "show_log": False,48                "use_gpu": False,49                "det_db_thresh": 0.2,       # Lower = detect more text regions50                "det_db_box_thresh": 0.3,    # Lower = keep more boxes51                "det_db_unclip_ratio": 1.8,  # Higher = bigger text boxes52                "rec_batch_num": 6,53            },54            "aggressive": {55                "use_angle_cls": True,56                "lang": "en",57                "show_log": False,58                "use_gpu": False,59                "det_db_thresh": 0.15,       # Very sensitive detection60                "det_db_box_thresh": 0.2,    # Keep almost all boxes61                "det_db_unclip_ratio": 2.0,  # Larger text regions62                "rec_batch_num": 6,63                "det_limit_side_len": 1920,  # Allow larger images64            },65        }66 67        cfg = configs.get(config_name, configs["default"])68        ocr_engines[config_name] = PaddleOCR(**cfg)69        logger.info(f"PaddleOCR model '{config_name}' loaded.")70 71    return ocr_engines[config_name]72 73 74@app.route("/health", methods=["GET"])75def health():76    """Health check endpoint."""77    return jsonify({"status": "ok", "engine": "paddleocr"})78 79 80@app.route("/ocr", methods=["POST"])81def ocr_extract():82    """Extract text from an uploaded image with multi-strategy OCR."""83    start_time = time.time()84 85    # Read image from request86    image_data = _read_image_from_request(request)87    if image_data is None:88        return jsonify({89            "success": False,90            "error": {"code": "no_image", "message": "No image provided"}91        }), 40092 93    try:94        # Preprocess image95        preprocess_meta = None96        image_np = None97 98        try:99            image_np, preprocess_meta = preprocess(image_data)100            logger.info(101                f"Preprocessing: blur={preprocess_meta['blur_score']:.3f}, "102                f"rotated={preprocess_meta['rotated']}, "103                f"cropped={preprocess_meta['cropped']}, "104                f"size={preprocess_meta['processed_size']}"105            )106        except Exception as preprocess_err:107            logger.warning(f"Preprocessing failed: {preprocess_err}")108 109        # Fallback: raw image decode if preprocessing failed110        if image_np is None:111            image = Image.open(io.BytesIO(image_data)).convert("RGB")112            image_np = np.array(image)113 114        # Strategy 1: Default OCR115        text_blocks = _run_ocr(image_np, "default")116 117        # Strategy 2: If too few results, try aggressive config118        if len(text_blocks) < 5:119            logger.info(120                f"Only {len(text_blocks)} blocks with default, "121                f"trying aggressive config..."122            )123            aggressive_blocks = _run_ocr(image_np, "aggressive")124            if len(aggressive_blocks) > len(text_blocks):125                text_blocks = aggressive_blocks126                logger.info(f"Aggressive config got {len(text_blocks)} blocks")127 128        # Strategy 3: If still few results, try without preprocessing (raw image)129        if len(text_blocks) < 5 and preprocess_meta is not None:130            logger.info("Trying OCR on raw image without preprocessing...")131            raw_image = Image.open(io.BytesIO(image_data)).convert("RGB")132            raw_np = np.array(raw_image)133            raw_blocks = _run_ocr(raw_np, "aggressive")134            if len(raw_blocks) > len(text_blocks):135                text_blocks = raw_blocks136                logger.info(f"Raw image got {len(text_blocks)} blocks")137 138        # Build response139        full_text_parts = [b["text"] for b in text_blocks]140        processing_time_ms = int((time.time() - start_time) * 1000)141 142        response = {143            "success": True,144            "data": {145                "text_blocks": text_blocks,146                "full_text": "\n".join(full_text_parts),147                "block_count": len(text_blocks),148                "processing_time_ms": processing_time_ms,149                "preprocessing": preprocess_meta,150            }151        }152 153        logger.info(154            f"OCR completed: {len(text_blocks)} blocks, "155            f"{processing_time_ms}ms"156        )157 158        return jsonify(response)159 160    except Exception as e:161        logger.error(f"OCR processing error: {e}")162        return jsonify({163            "success": False,164            "error": {"code": "processing_error", "message": str(e)}165        }), 500166 167 168def _run_ocr(image_np, config_name="default", max_retries=3):169    """Run PaddleOCR with retry + delay for memory recovery."""170    import gc171 172    for attempt in range(max_retries):173        try:174            engine = get_ocr_engine(config_name)175            results = engine.ocr(image_np, cls=True)176 177            text_blocks = []178            if results and results[0]:179                for line in results[0]:180                    bbox = line[0]181                    text = line[1][0]182                    confidence = float(line[1][1])183 184                    # Skip very low confidence results185                    if confidence < 0.3:186                        continue187 188                    text_blocks.append({189                        "text": text,190                        "confidence": confidence,191                        "bounding_box": {192                            "top_left": [int(bbox[0][0]), int(bbox[0][1])],193                            "top_right": [int(bbox[1][0]), int(bbox[1][1])],194                            "bottom_right": [int(bbox[2][0]), int(bbox[2][1])],195                            "bottom_left": [int(bbox[3][0]), int(bbox[3][1])],196                        }197                    })198 199            return text_blocks200 201        except Exception as e:202            logger.warning(f"OCR engine '{config_name}' attempt {attempt+1}/{max_retries} failed: {e}")203            # Force garbage collection and wait before retry204            gc.collect()205            if attempt < max_retries - 1:206                time.sleep(1)  # Wait 1s for memory to free up207 208    return []209 210 211def _read_image_from_request(req):212    """Extract image bytes from Flask request."""213    if "image" in req.files:214        return req.files["image"].read()215    if req.content_length and req.content_length > 0:216        return req.get_data()217    return None218 219 220if __name__ == "__main__":221    port = int(os.environ.get("OCR_SERVICE_PORT", 5001))222    logger.info(f"Starting OCR service on port {port}")223 224    # Pre-load default model on startup225    if os.environ.get("PRELOAD_MODEL", "true").lower() == "true":226        get_ocr_engine("default")227 228    app.run(host="0.0.0.0", port=port, debug=False)229