e904b667c6
PaddleOCR PR Tests GPU / test-pr-gpu (push) Blocked by required conditions
PaddleOCR PR Tests / test-pr (push) Blocked by required conditions
PaddleOCR PR Tests / test-pr-python (3.8) (push) Waiting to run
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Waiting to run
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / test-pr-python (3.13) (push) Waiting to run
PaddleOCR PR Tests / test-pr-python (3.9) (push) Waiting to run
212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from typing import Optional, Union
|
|
|
|
from .errors import (
|
|
APIError,
|
|
AuthError,
|
|
InvalidRequestError,
|
|
RateLimitError,
|
|
ResponseFormatError,
|
|
ServiceUnavailableError,
|
|
)
|
|
from .models import (
|
|
DocParsingOptions,
|
|
Model,
|
|
OCROptions,
|
|
PaddleOCRVLOptions,
|
|
PPStructureV3Options,
|
|
is_document_parsing_model,
|
|
is_ocr_model,
|
|
is_vl_model,
|
|
)
|
|
from .results import BatchStatus, Job, JobStatus, Progress
|
|
|
|
|
|
def validate_input_source(file_url: Optional[str], file_path: Optional[str]) -> None:
|
|
if not file_url and not file_path:
|
|
raise InvalidRequestError("Either file_url or file_path is required.")
|
|
if file_url and file_path:
|
|
raise InvalidRequestError("file_url and file_path are mutually exclusive.")
|
|
|
|
|
|
def default_payload(model: Model) -> dict:
|
|
if is_ocr_model(model):
|
|
return OCROptions().to_payload()
|
|
return resolve_document_options(model, None).to_payload()
|
|
|
|
|
|
def resolve_ocr_model(model: Union[Model, str]) -> Model:
|
|
resolved = resolve_model(model)
|
|
if not is_ocr_model(resolved):
|
|
raise InvalidRequestError(f"Unsupported OCR model: {model}")
|
|
return resolved
|
|
|
|
|
|
def resolve_document_model(model: Union[Model, str]) -> Model:
|
|
resolved = resolve_model(model)
|
|
if not is_document_parsing_model(resolved):
|
|
raise InvalidRequestError(f"Unsupported document parsing model: {model}")
|
|
return resolved
|
|
|
|
|
|
def resolve_model(model: Union[Model, str]) -> Model:
|
|
if isinstance(model, Model):
|
|
return model
|
|
try:
|
|
return Model(model)
|
|
except ValueError as e:
|
|
raise InvalidRequestError(f"Unsupported model: {model}") from e
|
|
|
|
|
|
def resolve_document_options(
|
|
model: Model, options: Optional[DocParsingOptions]
|
|
) -> DocParsingOptions:
|
|
if options is not None:
|
|
if model == Model.PP_STRUCTURE_V3 and not isinstance(
|
|
options, PPStructureV3Options
|
|
):
|
|
raise InvalidRequestError("PP-StructureV3 requires PPStructureV3Options.")
|
|
if is_vl_model(model) and not isinstance(options, PaddleOCRVLOptions):
|
|
raise InvalidRequestError("PaddleOCR-VL models require PaddleOCRVLOptions.")
|
|
return options
|
|
if model == Model.PP_STRUCTURE_V3:
|
|
return PPStructureV3Options()
|
|
return PaddleOCRVLOptions()
|
|
|
|
|
|
def job_id_for_task(job: Union[Job, str], task: str) -> str:
|
|
if isinstance(job, str):
|
|
return job
|
|
if job.task != task:
|
|
raise InvalidRequestError(
|
|
f"Job task mismatch: expected {task}, got {job.task}."
|
|
)
|
|
if task == "ocr" and not is_ocr_model(job.model):
|
|
raise InvalidRequestError(f"Job model is not an OCR model: {job.model}.")
|
|
if task == "document_parsing" and not is_document_parsing_model(job.model):
|
|
raise InvalidRequestError(
|
|
f"Job model is not a document parsing model: {job.model}."
|
|
)
|
|
return job.job_id
|
|
|
|
|
|
def extract_api_message_from_payload(payload: dict) -> Optional[str]:
|
|
for key in ("msg", "errorMsg", "message"):
|
|
value = payload.get(key)
|
|
if value:
|
|
return str(value)
|
|
data = payload.get("data")
|
|
if isinstance(data, dict):
|
|
value = data.get("errorMsg")
|
|
if value:
|
|
return str(value)
|
|
return None
|
|
|
|
|
|
def validate_state(data: dict) -> str:
|
|
state = data.get("state")
|
|
if state not in {"pending", "running", "done", "failed"}:
|
|
raise ResponseFormatError(f"Unknown or missing job state: {state}")
|
|
return state
|
|
|
|
|
|
def job_status_from_data(job_id: str, data: dict) -> JobStatus:
|
|
state = validate_state(data)
|
|
progress = None
|
|
ep = data.get("extractProgress")
|
|
if ep:
|
|
if not isinstance(ep, dict):
|
|
raise ResponseFormatError("'extractProgress' must be an object.")
|
|
progress = Progress(
|
|
total_pages=ep.get("totalPages", 0),
|
|
extracted_pages=ep.get("extractedPages", 0),
|
|
start_time=ep.get("startTime"),
|
|
end_time=ep.get("endTime"),
|
|
)
|
|
return JobStatus(
|
|
job_id=job_id,
|
|
state=state,
|
|
progress=progress,
|
|
result=data.get("resultUrl"),
|
|
error_msg=data.get("errorMsg"),
|
|
)
|
|
|
|
|
|
def raise_for_status(status_code: int, msg: str) -> None:
|
|
if 200 <= status_code < 300:
|
|
return
|
|
if status_code in (401, 403):
|
|
raise AuthError(f"Authentication failed: {msg}")
|
|
if status_code == 400:
|
|
raise InvalidRequestError(f"Bad request: {msg}")
|
|
if status_code == 429:
|
|
raise RateLimitError(f"Rate limit exceeded: {msg}")
|
|
if status_code in (503, 504):
|
|
raise ServiceUnavailableError(status_code, f"Service unavailable: {msg}")
|
|
raise APIError(status_code, msg)
|
|
|
|
|
|
def unwrap_api_response(payload: dict, status_code: int) -> dict:
|
|
if not isinstance(payload, dict):
|
|
raise ResponseFormatError("Response body must be a JSON object.")
|
|
code = payload.get("code", 0)
|
|
if code not in (0, None):
|
|
raise APIError(status_code, extract_api_message_from_payload(payload) or "")
|
|
data = payload.get("data")
|
|
if not isinstance(data, dict):
|
|
raise ResponseFormatError("Response JSON must contain object field 'data'.")
|
|
return data
|
|
|
|
|
|
def extract_job_id(data: dict) -> str:
|
|
job_id = data.get("jobId")
|
|
if not isinstance(job_id, str) or not job_id:
|
|
raise ResponseFormatError(
|
|
"Response data must contain non-empty string 'jobId'."
|
|
)
|
|
return job_id
|
|
|
|
|
|
def validate_result_json_url(data: dict) -> str:
|
|
result_url = data.get("resultUrl")
|
|
if not isinstance(result_url, dict):
|
|
raise ResponseFormatError("Done job response must contain object 'resultUrl'.")
|
|
json_url = result_url.get("jsonUrl")
|
|
if not isinstance(json_url, str) or not json_url:
|
|
raise ResponseFormatError(
|
|
"Done job response resultUrl must contain non-empty string 'jsonUrl'."
|
|
)
|
|
return json_url
|
|
|
|
|
|
def parse_batch_status(batch_id: str, data: dict) -> BatchStatus:
|
|
result = data.get("extractResult")
|
|
if not isinstance(result, list):
|
|
raise ResponseFormatError(
|
|
"Batch response data must contain list 'extractResult'."
|
|
)
|
|
jobs = []
|
|
for item in result:
|
|
if not isinstance(item, dict):
|
|
raise ResponseFormatError("Batch extractResult items must be objects.")
|
|
job_id = item.get("jobId")
|
|
if not isinstance(job_id, str) or not job_id:
|
|
raise ResponseFormatError(
|
|
"Batch extractResult items must contain non-empty string 'jobId'."
|
|
)
|
|
jobs.append(job_status_from_data(job_id, item))
|
|
return BatchStatus(batch_id=batch_id, jobs=jobs)
|