From 69f9a70ae51d08f4a24f27bcb857e7b001ad51b8 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Wed, 4 Feb 2026 12:35:14 +0800 Subject: [PATCH] feat: add omml api --- app/api/v1/endpoints/convert.py | 40 +++++++++++- app/api/v1/endpoints/image.py | 36 +--------- app/schemas/convert.py | 22 ++++++- app/schemas/image.py | 11 ---- test_omml_api.py | 112 ++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 47 deletions(-) create mode 100644 test_omml_api.py diff --git a/app/api/v1/endpoints/convert.py b/app/api/v1/endpoints/convert.py index ea381fd..e3575ad 100644 --- a/app/api/v1/endpoints/convert.py +++ b/app/api/v1/endpoints/convert.py @@ -1,10 +1,10 @@ -"""Markdown to DOCX conversion endpoint.""" +"""Format conversion endpoints.""" from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import Response from app.core.dependencies import get_converter -from app.schemas.convert import MarkdownToDocxRequest +from app.schemas.convert import MarkdownToDocxRequest, LatexToOmmlRequest, LatexToOmmlResponse from app.services.converter import Converter router = APIRouter() @@ -28,3 +28,39 @@ async def convert_markdown_to_docx( ) except Exception as e: raise HTTPException(status_code=500, detail=f"Conversion failed: {e}") + + +@router.post("/latex-to-omml", response_model=LatexToOmmlResponse) +async def convert_latex_to_omml( + request: LatexToOmmlRequest, + converter: Converter = Depends(get_converter), +) -> LatexToOmmlResponse: + """Convert LaTeX formula to OMML (Office Math Markup Language). + + OMML is the math format used by Microsoft Word and other Office applications. + This endpoint is separate from the main OCR endpoint due to the performance + overhead of OMML conversion (requires creating a temporary DOCX file). + + Args: + request: Contains the LaTeX formula to convert (without $ or $$ delimiters). + + Returns: + OMML representation of the formula. + + Example: + ```bash + curl -X POST "http://localhost:8000/api/v1/convert/latex-to-omml" \\ + -H "Content-Type: application/json" \\ + -d '{"latex": "\\\\frac{a}{b} + \\\\sqrt{c}"}' + ``` + """ + if not request.latex or not request.latex.strip(): + raise HTTPException(status_code=400, detail="LaTeX formula cannot be empty") + + try: + omml = converter.convert_to_omml(request.latex) + return LatexToOmmlResponse(omml=omml) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except RuntimeError as e: + raise HTTPException(status_code=503, detail=str(e)) diff --git a/app/api/v1/endpoints/image.py b/app/api/v1/endpoints/image.py index 3c18f92..87f7eb6 100644 --- a/app/api/v1/endpoints/image.py +++ b/app/api/v1/endpoints/image.py @@ -2,12 +2,11 @@ from fastapi import APIRouter, Depends, HTTPException -from app.core.dependencies import get_image_processor, get_layout_detector, get_ocr_service, get_mineru_ocr_service, get_converter -from app.schemas.image import ImageOCRRequest, ImageOCRResponse, LatexToOmmlRequest, LatexToOmmlResponse +from app.core.dependencies import get_image_processor, get_layout_detector, get_ocr_service, get_mineru_ocr_service +from app.schemas.image import ImageOCRRequest, ImageOCRResponse from app.services.image_processor import ImageProcessor from app.services.layout_detector import LayoutDetector from app.services.ocr_service import OCRService, MineruOCRService -from app.services.converter import Converter router = APIRouter() @@ -31,7 +30,7 @@ async def process_image_ocr( 4. Convert output to LaTeX, Markdown, and MathML formats Note: OMML conversion is not included due to performance overhead. - Use the /latex-to-omml endpoint to convert LaTeX to OMML separately. + Use the /convert/latex-to-omml endpoint to convert LaTeX to OMML separately. """ image = image_processor.preprocess( @@ -55,32 +54,3 @@ async def process_image_ocr( mathml=ocr_result.get("mathml", ""), mml=ocr_result.get("mml", ""), ) - - -@router.post("/latex-to-omml", response_model=LatexToOmmlResponse) -async def convert_latex_to_omml( - request: LatexToOmmlRequest, - converter: Converter = Depends(get_converter), -) -> LatexToOmmlResponse: - """Convert LaTeX formula to OMML (Office Math Markup Language). - - OMML is the math format used by Microsoft Word and other Office applications. - This endpoint is separate from the main OCR endpoint due to the performance - overhead of OMML conversion (requires creating a temporary DOCX file). - - Args: - request: Contains the LaTeX formula to convert (without $ or $$ delimiters). - - Returns: - OMML representation of the formula. - """ - if not request.latex or not request.latex.strip(): - raise HTTPException(status_code=400, detail="LaTeX formula cannot be empty") - - try: - omml = converter.convert_to_omml(request.latex) - return LatexToOmmlResponse(omml=omml) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) - except RuntimeError as e: - raise HTTPException(status_code=503, detail=str(e)) diff --git a/app/schemas/convert.py b/app/schemas/convert.py index 97f933e..068ceaa 100644 --- a/app/schemas/convert.py +++ b/app/schemas/convert.py @@ -1,4 +1,4 @@ -"""Request and response schemas for markdown to DOCX conversion endpoint.""" +"""Request and response schemas for format conversion endpoints.""" from pydantic import BaseModel, Field, field_validator @@ -17,3 +17,23 @@ class MarkdownToDocxRequest(BaseModel): raise ValueError("Markdown content cannot be empty") return v + +class LatexToOmmlRequest(BaseModel): + """Request body for LaTeX to OMML conversion endpoint.""" + + latex: str = Field(..., description="Pure LaTeX formula (without $ or $$ delimiters)") + + @field_validator("latex") + @classmethod + def validate_latex_not_empty(cls, v: str) -> str: + """Validate that LaTeX formula is not empty.""" + if not v or not v.strip(): + raise ValueError("LaTeX formula cannot be empty") + return v + + +class LatexToOmmlResponse(BaseModel): + """Response body for LaTeX to OMML conversion endpoint.""" + + omml: str = Field("", description="OMML (Office Math Markup Language) representation") + diff --git a/app/schemas/image.py b/app/schemas/image.py index fb8946f..3b46a18 100644 --- a/app/schemas/image.py +++ b/app/schemas/image.py @@ -47,14 +47,3 @@ class ImageOCRResponse(BaseModel): layout_info: LayoutInfo = Field(default_factory=LayoutInfo) recognition_mode: str = Field("", description="Recognition mode used: mixed_recognition or formula_recognition") - -class LatexToOmmlRequest(BaseModel): - """Request body for LaTeX to OMML conversion endpoint.""" - - latex: str = Field(..., description="Pure LaTeX formula (without $ or $$ delimiters)") - - -class LatexToOmmlResponse(BaseModel): - """Response body for LaTeX to OMML conversion endpoint.""" - - omml: str = Field("", description="OMML (Office Math Markup Language) representation") diff --git a/test_omml_api.py b/test_omml_api.py new file mode 100644 index 0000000..dd78a84 --- /dev/null +++ b/test_omml_api.py @@ -0,0 +1,112 @@ +"""Test script for OMML conversion API endpoint.""" + +import requests +import json + + +def test_latex_to_omml(): + """Test the /convert/latex-to-omml endpoint.""" + + # Test cases + test_cases = [ + { + "name": "Simple fraction", + "latex": "\\frac{a}{b}", + }, + { + "name": "Quadratic formula", + "latex": "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}", + }, + { + "name": "Integral", + "latex": "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}", + }, + { + "name": "Matrix", + "latex": "\\begin{matrix} a & b \\\\ c & d \\end{matrix}", + }, + ] + + base_url = "http://localhost:8000/api/v1/convert/latex-to-omml" + + print("Testing OMML Conversion API") + print("=" * 80) + + for i, test_case in enumerate(test_cases, 1): + print(f"\nTest {i}: {test_case['name']}") + print("-" * 80) + print(f"LaTeX: {test_case['latex']}") + + try: + response = requests.post( + base_url, + json={"latex": test_case["latex"]}, + headers={"Content-Type": "application/json"}, + timeout=10, + ) + + if response.status_code == 200: + result = response.json() + omml = result.get("omml", "") + + print(f"✓ Status: {response.status_code}") + print(f"OMML length: {len(omml)} characters") + print(f"OMML preview: {omml[:150]}...") + + else: + print(f"✗ Status: {response.status_code}") + print(f"Error: {response.text}") + + except requests.exceptions.RequestException as e: + print(f"✗ Request failed: {e}") + except Exception as e: + print(f"✗ Error: {e}") + + print("\n" + "=" * 80) + + +def test_invalid_input(): + """Test error handling with invalid input.""" + + print("\nTesting Error Handling") + print("=" * 80) + + base_url = "http://localhost:8000/api/v1/convert/latex-to-omml" + + # Empty LaTeX + print("\nTest: Empty LaTeX") + response = requests.post( + base_url, + json={"latex": ""}, + headers={"Content-Type": "application/json"}, + ) + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}") + + # Missing LaTeX field + print("\nTest: Missing LaTeX field") + response = requests.post( + base_url, + json={}, + headers={"Content-Type": "application/json"}, + ) + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}") + + print("\n" + "=" * 80) + + +if __name__ == "__main__": + print("OMML API Test Suite") + print("Make sure the API server is running on http://localhost:8000") + print() + + try: + test_latex_to_omml() + test_invalid_input() + print("\n✓ All tests completed!") + + except KeyboardInterrupt: + print("\n\n✗ Tests interrupted by user") + except Exception as e: + print(f"\n✗ Test suite failed: {e}")