Files
OCR/backend/tests/services/test_layout_model.py
egg 6235280c45 feat: upgrade PP-StructureV3 models to latest versions
- Layout: PP-DocLayout-S → PP-DocLayout_plus-L (83.2% mAP)
- Table: Single model → Dual SLANeXt (wired/wireless)
- Formula: PP-FormulaNet_plus-L for enhanced recognition
- Add preprocessing flags support (orientation, unwarping)
- Update frontend i18n descriptions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 14:22:06 +08:00

245 lines
9.8 KiB
Python

"""
Unit tests for Layout Model Selection feature in OCR Service.
This replaces the deprecated PP-StructureV3 parameter tests.
"""
import pytest
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
# Mock all external dependencies before importing OCRService
sys.modules['paddleocr'] = MagicMock()
sys.modules['PIL'] = MagicMock()
sys.modules['pdf2image'] = MagicMock()
# Mock paddle with version attribute
paddle_mock = MagicMock()
paddle_mock.__version__ = '2.5.0'
paddle_mock.device.get_device.return_value = 'cpu'
paddle_mock.device.get_available_device.return_value = 'cpu'
sys.modules['paddle'] = paddle_mock
# Mock torch
torch_mock = MagicMock()
torch_mock.cuda.is_available.return_value = False
sys.modules['torch'] = torch_mock
from app.services.ocr_service import OCRService, LAYOUT_MODEL_MAPPING, _USE_PUBLAYNET_DEFAULT
from app.core.config import settings
class TestLayoutModelMapping:
"""Test layout model name mapping"""
def test_layout_model_mapping_exists(self):
"""Verify LAYOUT_MODEL_MAPPING constant exists and has correct values"""
assert 'chinese' in LAYOUT_MODEL_MAPPING
assert 'default' in LAYOUT_MODEL_MAPPING
assert 'cdla' in LAYOUT_MODEL_MAPPING
def test_chinese_model_maps_to_pp_doclayout(self):
"""Verify 'chinese' maps to PP-DocLayout_plus-L"""
assert LAYOUT_MODEL_MAPPING['chinese'] == 'PP-DocLayout_plus-L'
def test_default_model_maps_to_publaynet_sentinel(self):
"""Verify 'default' maps to sentinel value for PubLayNet default"""
# The 'default' model uses a sentinel value that signals "use PubLayNet default (no custom model)"
assert LAYOUT_MODEL_MAPPING['default'] == _USE_PUBLAYNET_DEFAULT
def test_cdla_model_maps_to_picodet(self):
"""Verify 'cdla' maps to picodet_lcnet_x1_0_fgd_layout_cdla"""
assert LAYOUT_MODEL_MAPPING['cdla'] == 'picodet_lcnet_x1_0_fgd_layout_cdla'
class TestLayoutModelEngine:
"""Test engine creation with different layout models"""
def test_chinese_model_creates_engine_with_pp_doclayout(self):
"""Verify 'chinese' layout model uses PP-DocLayout_plus-L"""
ocr_service = OCRService()
with patch.object(ocr_service, 'structure_engine', None):
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
engine = ocr_service._ensure_structure_engine(layout_model='chinese')
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
assert call_kwargs.get('layout_detection_model_name') == 'PP-DocLayout_plus-L'
def test_default_model_creates_engine_without_model_name(self):
"""Verify 'default' layout model does not specify model name (uses default)"""
ocr_service = OCRService()
with patch.object(ocr_service, 'structure_engine', None):
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
engine = ocr_service._ensure_structure_engine(layout_model='default')
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
# For 'default', layout_detection_model_name should be None or not set
assert call_kwargs.get('layout_detection_model_name') is None
def test_cdla_model_creates_engine_with_picodet(self):
"""Verify 'cdla' layout model uses picodet_lcnet_x1_0_fgd_layout_cdla"""
ocr_service = OCRService()
with patch.object(ocr_service, 'structure_engine', None):
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
engine = ocr_service._ensure_structure_engine(layout_model='cdla')
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
assert call_kwargs.get('layout_detection_model_name') == 'picodet_lcnet_x1_0_fgd_layout_cdla'
def test_none_layout_model_uses_chinese_default(self):
"""Verify None layout_model defaults to 'chinese' model"""
ocr_service = OCRService()
with patch.object(ocr_service, 'structure_engine', None):
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# Pass None for layout_model
engine = ocr_service._ensure_structure_engine(layout_model=None)
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
# Should use 'chinese' model as default
assert call_kwargs.get('layout_detection_model_name') == 'PP-DocLayout_plus-L'
class TestLayoutModelCaching:
"""Test engine caching behavior with layout models"""
def test_same_layout_model_uses_cached_engine(self):
"""Verify same layout model reuses cached engine"""
ocr_service = OCRService()
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# First call with 'chinese'
engine1 = ocr_service._ensure_structure_engine(layout_model='chinese')
# Second call with same model should use cache
engine2 = ocr_service._ensure_structure_engine(layout_model='chinese')
# Verify only one engine was created
assert mock_ppstructure.call_count == 1
assert engine1 is engine2
def test_different_layout_model_creates_new_engine(self):
"""Verify different layout model creates new engine"""
ocr_service = OCRService()
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine1 = Mock()
mock_engine2 = Mock()
mock_ppstructure.side_effect = [mock_engine1, mock_engine2]
# First call with 'chinese'
engine1 = ocr_service._ensure_structure_engine(layout_model='chinese')
# Second call with 'cdla' should create new engine
engine2 = ocr_service._ensure_structure_engine(layout_model='cdla')
# Verify two engines were created
assert mock_ppstructure.call_count == 2
assert engine1 is not engine2
class TestLayoutModelFlow:
"""Test layout model parameter flow through processing pipeline"""
def test_layout_model_passed_to_engine_creation(self):
"""Verify layout_model is passed through to _ensure_structure_engine"""
ocr_service = OCRService()
# Test that _ensure_structure_engine accepts layout_model parameter
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# Call with specific layout_model
engine = ocr_service._ensure_structure_engine(layout_model='cdla')
# Verify correct model was requested
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
assert call_kwargs.get('layout_detection_model_name') == 'picodet_lcnet_x1_0_fgd_layout_cdla'
def test_layout_model_default_behavior(self):
"""Verify default layout model behavior when None is passed"""
ocr_service = OCRService()
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# Call without layout_model (None)
engine = ocr_service._ensure_structure_engine(layout_model=None)
# Should use config default (PP-DocLayout-S)
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
assert call_kwargs.get('layout_detection_model_name') == settings.layout_detection_model_name
def test_layout_model_unknown_value_falls_back(self):
"""Verify unknown layout model falls back to config default"""
ocr_service = OCRService()
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# Call with unknown layout_model
engine = ocr_service._ensure_structure_engine(layout_model='unknown_model')
# Should use config default
mock_ppstructure.assert_called_once()
call_kwargs = mock_ppstructure.call_args[1]
assert call_kwargs.get('layout_detection_model_name') == settings.layout_detection_model_name
class TestLayoutModelLogging:
"""Test layout model logging"""
def test_layout_model_is_logged(self):
"""Verify layout model selection is logged"""
ocr_service = OCRService()
with patch('app.services.ocr_service.PPStructureV3') as mock_ppstructure:
with patch('app.services.ocr_service.logger') as mock_logger:
mock_engine = Mock()
mock_ppstructure.return_value = mock_engine
# Call with specific layout_model
ocr_service._ensure_structure_engine(layout_model='cdla')
# Verify logging occurred
assert mock_logger.info.call_count >= 1
# Check that model name was logged
log_calls = [str(call) for call in mock_logger.info.call_args_list]
assert any('cdla' in str(call).lower() or 'layout' in str(call).lower() for call in log_calls)
if __name__ == '__main__':
pytest.main([__file__, '-v'])