I’m not able to set a custom path for doc_orientation_classify_model_dir in an air-gapped network. It always defaults to the local cache path. How can I modify it to use my given model path?

#1
by Vikas19 - opened

I need to run PaddleOCR in an air-gapped network (no internet).
I’ve already pre-downloaded the models and configured paths, and everything works fine for most of the models.

However, I’m stuck on one part:
👉 The property doc_orientation_classify_model_dir is not picking up my custom path and always falls back to the default cache directory.

I’ve been struggling with this for some time.

Here’s my service code snippet:

import logging
import os
import tempfile
from pathlib import Path
from typing import List, Any, Optional

from paddleocr import PPStructureV3

from app.config.settings import get_settings

logger = logging.getLogger(name)
settings = get_settings()

class OCRService:
"""OCR service for text extraction from images"""

def init(self):
self.ocr = None
self.MODEL_PATHS = [
Path(file).resolve().parents[1] / "models" / ".paddlex" / "official_models",
Path("/app/app/models/.paddlex/official_models"),
Path("/app/app/models/.paddlex"),
Path(file).resolve().parents[1] / "models" / ".paddlex",
Path(file).resolve().parents[1] / "models"
]
self.BASE_MODEL_DIR = None

def _find_model_directory(self) -> Optional[Path]:
"""Find the correct model directory"""
for path in self.MODEL_PATHS:
logger.info(f"Checking model path: {path}")
if path.exists() and path.is_dir():
# Check if it has any model subdirectories
subdirs = [d for d in path.iterdir() if d.is_dir()]
if subdirs:
logger.info(f"Found model directory: {path} with {len(subdirs)} subdirectories")
return path
else:
logger.info(f"Path exists but no model subdirectories found: {path}")
else:
logger.info(f"Path does not exist: {path}")

logger.warning("No model directory found")
return None

def initialize(self) -> bool:
"""Initialize OCR engine"""
self.BASE_MODEL_DIR = self._find_model_directory()
try:
self.ocr = PPStructureV3(
use_doc_orientation_classify=True,
use_textline_orientation=True,
lang=settings.OCR_LANGUAGE or 'en',

        # Layout
        layout_detection_model_name=None,
        layout_detection_model_dir=self.BASE_MODEL_DIR / "PP-DocLayout_plus-L",

        # Region detection (disable if you don't have a region model)
        region_detection_model_name=None,
        region_detection_model_dir=self.BASE_MODEL_DIR / "PP-DocBlockLayout",

        # Chart recognition
        chart_recognition_model_name=None,
        chart_recognition_model_dir=self.BASE_MODEL_DIR / "PP-Chart2Table",

        # Doc orientation classify
        doc_orientation_classify_model_name=None,
        doc_orientation_classify_model_dir=self.BASE_MODEL_DIR / "PP-LCNet_x1_0_doc_ori",


        # Text detection
        text_detection_model_name=None,
        text_detection_model_dir=self.BASE_MODEL_DIR / "PP-OCRv5_server_det",

        # Textline orientation
        textline_orientation_model_name=None,
        textline_orientation_model_dir=self.BASE_MODEL_DIR / "PP-LCNet_x1_0_textline_ori",

        # Text recognition
        text_recognition_model_name=None,
        text_recognition_model_dir=self.BASE_MODEL_DIR / "PP-OCRv5_server_rec",

        # Table recognition
        table_classification_model_name=None,
        table_classification_model_dir=self.BASE_MODEL_DIR / "PP-LCNet_x1_0_table_cls",
        wired_table_structure_recognition_model_name=None,
        wired_table_structure_recognition_model_dir=self.BASE_MODEL_DIR / "SLANeXt_wired",
        wireless_table_structure_recognition_model_name=None,
        wireless_table_structure_recognition_model_dir=self.BASE_MODEL_DIR / "SLANet_plus",
        wired_table_cells_detection_model_name=None,
        wired_table_cells_detection_model_dir=self.BASE_MODEL_DIR / "RT-DETR-L_wired_table_cell_det",
        wireless_table_cells_detection_model_name=None,
        wireless_table_cells_detection_model_dir=self.BASE_MODEL_DIR / "RT-DETR-L_wireless_table_cell_det",

        # Formula recognition
        formula_recognition_model_name=None,
        formula_recognition_model_dir=self.BASE_MODEL_DIR / "PP-FormulaNet_plus-L"
    )

    logger.info("OCR service initialized successfully")
    return True
except Exception as e:
    logger.error(f"Failed to initialize OCR service: {e}")
    return False

async def extract_text_from_image(self, image_content: bytes, filename: str = None) -> List[str]:
"""Extract text from image content"""
if not self.ocr:
raise RuntimeError("OCR service not initialized")

tmp_path = None
try:
    # Save image to temporary file
    suffix = self._get_file_suffix(filename)
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
        tmp.write(image_content)
        tmp_path = tmp.name

    # Run OCR
    ocr_result = self.ocr.predict(tmp_path)
    logger.info(f"OCR processing completed for {filename or 'uploaded file'}")

    # Extract text from results
    rec_texts = self._extract_texts_from_result(ocr_result)

    # Remove duplicates while preserving order
    unique_texts = self._remove_duplicates(rec_texts)

    logger.info(f"Extracted {len(unique_texts)} unique text lines")
    return unique_texts

finally:
    # Clean up temporary file
    if tmp_path and os.path.exists(tmp_path):
        try:
            os.remove(tmp_path)
        except Exception as e:
            logger.warning(f"Could not delete temporary file {tmp_path}: {e}")

def _extract_texts_from_result(self, ocr_result: Any) -> List[str]:
"""Extract text lines from OCR result"""
rec_texts = []

if isinstance(ocr_result, list) and len(ocr_result) > 0:
    result = ocr_result[0]

    # Method 1: Extract from overall_ocr_res
    if 'overall_ocr_res' in result and 'rec_texts' in result['overall_ocr_res']:
        rec_texts.extend(result['overall_ocr_res']['rec_texts'])
        logger.debug(f"Extracted {len(result['overall_ocr_res']['rec_texts'])} texts from overall_ocr_res")

    # Method 2: Extract from parsing_res_list
    if 'parsing_res_list' in result:
        for parsing_item in result['parsing_res_list']:
            if isinstance(parsing_item, dict) and 'content' in parsing_item:
                content_lines = parsing_item['content'].strip().split('\n')
                rec_texts.extend([line.strip() for line in content_lines if line.strip()])
        logger.debug("Additional texts extracted from parsing_res_list")

else:
    # Fallback: Handle original format
    for page in ocr_result:
        if isinstance(page, dict) and "rec_texts" in page:
            rec_texts.extend(page["rec_texts"])
        else:
            for line in page:
                if len(line) >= 2:
                    rec_texts.append(line[1][0])

return rec_texts

def _remove_duplicates(self, texts: List[str]) -> List[str]:
"""Remove duplicates while preserving order"""
seen = set()
unique_texts = []
for text in texts:
if text not in seen:
seen.add(text)
unique_texts.append(text)
return unique_texts

def _get_file_suffix(self, filename: str) -> str:
"""Get appropriate file suffix"""
if filename:
_, ext = os.path.splitext(filename)
return ext if ext else ".jpg"
return ".jpg"
main class:

import os
import sys

===== CRITICAL: SET ENVIRONMENT VARIABLES FIRST =====
These MUST be set before any PaddleOCR imports
def setup_offline_environment():
"""Setup environment variables for offline mode"""
offline_env = {
'PADDLEX_OFFLINE_MODE': '1',
'PADDLEX_HOME': os.environ.get('PADDLEX_HOME', '/app/models/.paddlex'),
'PADDLEX_CACHE_DIR': os.environ.get('PADDLEX_CACHE_DIR', '/app/models/.paddlex/temp'),
'PADDLE_HUB_HOME': os.environ.get('PADDLE_HUB_HOME', '/app/models/.paddlex/official_models'),
'HUB_HOME': os.environ.get('HUB_HOME', '/app/models/.paddlex/official_models'),
'PADDLEOCR_MCP_PPOCR_SOURCE': 'LOCAL',
'PADDLE_OCR_MODEL_DOWNLOAD': '0',
'PADDLE_DISABLE_TELEMETRY': '1',
'REQUESTS_TIMEOUT': '1', # Quick timeout for network requests
'REQUESTS_RETRIES': '0', # No retries
}

for key, value in offline_env.items():
if key not in os.environ: # Don't override existing env vars
os.environ[key] = value

Verify critical paths exist

critical_paths = [
os.environ['PADDLEX_HOME'],
os.environ['PADDLE_HUB_HOME'],
os.environ['PADDLEX_CACHE_DIR']
]

for path in critical_paths:
if not os.path.exists(path):
print(f"Warning: Model path does not exist: {path}")
os.makedirs(path, exist_ok=True)
Setup offline environment FIRST
setup_offline_environment()

Now import everything else
from contextlib import asynccontextmanager
import logging

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles

from app.api.v1.api import api_router
from app.config.logging_config import setup_logging
from app.config.logging_middleware import LoggingMiddleware
from app.config.settings import get_settings
from app.services.ocr_service import OCRService

Setup logging first
setup_logging()
logger = logging.getLogger(name)

settings = get_settings()

@asynccontextmanager
async def lifespan(app: FastAPI):
try:

Log environment status

logger.info("=== Environment Status ===")
logger.info(f"PADDLEX_OFFLINE_MODE: {os.environ.get('PADDLEX_OFFLINE_MODE')}")
logger.info(f"PADDLEX_HOME: {os.environ.get('PADDLEX_HOME')}")
logger.info(f"PADDLE_HUB_HOME: {os.environ.get('PADDLE_HUB_HOME')}")
logger.info(f"PADDLEOCR_MCP_PPOCR_SOURCE: {os.environ.get('PADDLEOCR_MCP_PPOCR_SOURCE')}")

# Verify model paths exist
model_paths = [
    os.environ.get('PADDLEX_HOME'),
    os.environ.get('PADDLE_HUB_HOME'),
    os.environ.get('PADDLEX_CACHE_DIR')
]

for path in model_paths:
    if path and os.path.exists(path):
        file_count = sum(1 for _ in os.walk(path) if _[2])  # Count files
        logger.info(f"Model path {path} exists with {file_count} files")
    else:
        logger.warning(f"Model path {path} does not exist or is empty")

# Initialize OCR service
logger.info("Initializing OCR service...")
ocr_service = OCRService()

if not ocr_service.initialize():
    raise RuntimeError("Failed to initialize OCR service")

app.state.ocr_service = ocr_service
logger.info("OCR service initialized successfully")

# 🔑 FIXED: Copy state to sub_app if it exists
if hasattr(app, "sub_app") and app.sub_app:
    app.sub_app.state.ocr_service = ocr_service
    logger.info("Services copied to sub_app state")

except Exception as e:
logger.error(f"Failed to initialize application: {e}")
logger.error(f"Error type: {type(e).name}")
# Print more detailed error information
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise

yield
logger.info("=== Passport MRZ OCR API Shutting Down ===")

Clean up services if needed

try:
if hasattr(app.state, 'ocr_service'):
# Add any OCR service cleanup here if needed
logger.info("OCR service cleaned up")

if hasattr(app.state, 'face_service'):
    # Add any Face service cleanup here if needed
    logger.info("Face service cleaned up")

except Exception as e:
logger.error(f"Error during cleanup: {e}")
def create_application() -> FastAPI:
"""Create FastAPI application with configuration"""

Create main app

app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION,
lifespan=lifespan,
docs_url=None, # Custom docs
redoc_url=None, # Custom redoc
openapi_url=None # Custom openapi
)

Add middleware

app.add_middleware(LoggingMiddleware)

app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_HOSTS,
allow_credentials=True,
allow_methods=[""],
allow_headers=["
"],
)

Mount static files

if os.path.exists("static"):
if settings.CONTEXT_PATH:
app.mount(
f"{settings.CONTEXT_PATH}/static",
StaticFiles(directory="static"),
name="static"
)
else:
app.mount("/static", StaticFiles(directory="static"), name="static")
logger.info("Static files mounted")

Create sub-app if context path is configured

if settings.CONTEXT_PATH:
sub_app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION,
docs_url=None,
redoc_url=None,
)

sub_app.include_router(api_router)

# 🔑 FIXED: Store reference to sub_app BEFORE mounting
app.sub_app = sub_app

app.mount(settings.CONTEXT_PATH, sub_app)

# Root endpoint for main app
@app .get("/")
async def root():
    return {
        "message": f"Welcome to {settings.PROJECT_NAME}!",
        "version": settings.VERSION,
        "context_path": settings.CONTEXT_PATH,
        "api_endpoints": f"API available at {settings.CONTEXT_PATH}/",
        "documentation": f"Documentation available at {settings.CONTEXT_PATH}/docs-simple",
        "status": "running"
    }

return app, sub_app

else:
# Include API router in main app
app.include_router(api_router)
# Set sub_app to None when not using context path
app.sub_app = None

return app, app

Create the application
app, working_app = create_application()

Export both for use in use in other modules
all = ["app", "working_app"]

log :

Creating model: ('PP-LCNet_x1_0_doc_ori', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-LCNet_x1_0_doc_ori'))
WARNING: Logging before InitGoogleLogging() is written to STDERR
I0925 17:20:08.887140 1780 onednn_context.cc:81] oneDNN v3.6.2
Creating model: ('PP-DocBlockLayout', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-DocBlockLayout'))
Creating model: ('PP-DocLayout_plus-L', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-DocLayout_plus-L'))
Creating model: ('PP-LCNet_x1_0_textline_ori', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-LCNet_x1_0_textline_ori'))
Creating model: ('PP-OCRv5_server_det', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-OCRv5_server_det'))
Creating model: ('PP-OCRv5_server_rec', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-OCRv5_server_rec'))
Creating model: ('PP-LCNet_x1_0_table_cls', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-LCNet_x1_0_table_cls'))
Creating model: ('SLANeXt_wired', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/SLANeXt_wired'))
Creating model: ('SLANet_plus', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/SLANet_plus'))
Creating model: ('RT-DETR-L_wired_table_cell_det', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/RT-DETR-L_wired_table_cell_det'))
Creating model: ('RT-DETR-L_wireless_table_cell_det', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/RT-DETR-L_wireless_table_cell_det'))
Creating model: ('PP-FormulaNet_plus-L', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-FormulaNet_plus-L'))
Creating model: ('PP-Chart2Table', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-Chart2Table'))
Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.
Loading configuration file E:\project_workspace\passport_ocr\passport_ocr\app\models.paddlex\official_models\PP-Chart2Table\config.json
Loading weights file E:\project_workspace\passport_ocr\passport_ocr\app\models.paddlex\official_models\PP-Chart2Table\model_state.pdparams
Loaded weights file from disk, setting weights to model.
All model checkpoint weights were used when initializing PPChart2TableInference.

All the weights of PPChart2TableInference were initialized from the model checkpoint at E:\project_workspace\passport_ocr\passport_ocr\app\models.paddlex\official_models\PP-Chart2Table.
If your task is similar to the task the model of the checkpoint was trained on, you can already use PPChart2TableInference for predictions without further training.
Loading configuration file E:\project_workspace\passport_ocr\passport_ocr\app\models.paddlex\official_models\PP-Chart2Table\generation_config.json
TRACE: ASGI [1] Send {'type': 'lifespan.startup.complete'}
INFO: Application startup complete.

Creating model: ('PP-LCNet_x1_0_doc_ori', None)
Using official model (PP-LCNet_x1_0_doc_ori), the model files will be automatically downloaded and saved in C:\Users\HP.paddlex\official_models\PP-LCNet_x1_0_doc_ori.
inference.yml: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 766/766 [00:00<00:00, 9.71MB/s]
README.md: 6.85kB [00:00, 27.4MB/s] | 0.00/766 [00:00<?, ?B/s]
inference.json: 104kB [00:00, 199MB/s]
config.json: 2.56kB [00:00, 19.8MB/s]
.gitattributes: 1.57kB [00:00, 13.2MB/s]
inference.pdiparams: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6.75M/6.75M [00:00<00:00, 9.33MB/s]
Fetching 6 files: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6/6 [00:02<00:00, 2.63it/s]
Creating model: ('PP-LCNet_x1_0_textline_ori', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-LCNet_x1_0_textline_ori'))
Creating model: ('PP-OCRv5_server_det', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-OCRv5_server_det'))
Creating model: ('PP-OCRv5_server_rec', WindowsPath('E:/project_workspace/passport_ocr/passport_ocr/app/models/.paddlex/official_models/PP-OCRv5_server_rec'))
2025-09-25 17:24:22,137 - app.middleware - WARNING - [3d75c894] SLOW REQUEST: POST /passport-ocr/api/v1/upload-passport took 81.7024s

This issue occurs even though I already have a valid model stored in my local directory. please help me

Sign up or log in to comment