Files
wehub-resource-sync e904b667c6
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 / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

154 lines
4.3 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.
"""Shared user-facing input contract for all MCP inference providers."""
from __future__ import annotations
import tempfile
from contextlib import contextmanager
from enum import Enum
from pathlib import Path
from typing import Generator, Optional
from ...utils import (
decode_base64_payload,
extract_base64_payload,
infer_file_type_from_bytes,
is_base64,
is_url,
)
COMMON_INPUT_DESCRIPTION = (
"Supported input_data: absolute file path accessible to the MCP server process "
"(~ is expanded), HTTP(S) URL, raw Base64, or data URL. Relative paths are "
"rejected. Backend-specific conversion is handled by the MCP server."
)
_INPUT_EXTENSIONS = frozenset(
{
".jpg",
".jpeg",
".png",
".pdf",
".bmp",
".webp",
".tiff",
".tif",
".gif",
}
)
_SUFFIX_BY_FILE_TYPE = {
"image": ".png",
"pdf": ".pdf",
}
class InputKind(str, Enum):
URL = "url"
ABSOLUTE_PATH = "absolute_path"
BASE64 = "base64"
DATA_URL = "data_url"
def normalize(input_data: str) -> str:
return input_data.strip()
def looks_like_file_path(value: str) -> bool:
if is_url(value) or value.startswith("data:"):
return False
if value.startswith("~") or "/" in value or "\\" in value:
return True
return Path(value).suffix.lower() in _INPUT_EXTENSIONS
def classify_input(value: str) -> InputKind:
if is_url(value):
return InputKind.URL
if value.startswith("data:"):
payload = extract_base64_payload(value)
if not is_base64(payload):
raise ValueError("Invalid data URL: payload is not valid Base64.")
return InputKind.DATA_URL
if is_base64(value):
return InputKind.BASE64
if looks_like_file_path(value):
return InputKind.ABSOLUTE_PATH
raise ValueError(f"Invalid input_data. {COMMON_INPUT_DESCRIPTION}")
def resolve_absolute_path(value: str) -> Path:
path = Path(value).expanduser()
if not path.is_absolute():
raise ValueError(
f"Relative paths are not supported: {value!r}. "
"Use an absolute path accessible to the MCP server process."
)
if not path.exists():
raise ValueError(
f"File not found: {value!r} (resolved to {path}). "
f"{COMMON_INPUT_DESCRIPTION}"
)
return path.resolve()
def validate_external_contract(input_data: str) -> InputKind:
value = normalize(input_data)
if not value:
raise ValueError("input_data must not be empty.")
kind = classify_input(value)
if kind is InputKind.ABSOLUTE_PATH:
resolve_absolute_path(value)
return kind
if kind in (InputKind.BASE64, InputKind.DATA_URL):
payload = extract_base64_payload(value)
data = decode_base64_payload(payload)
file_type = infer_file_type_from_bytes(data)
if file_type not in ("image", "pdf"):
raise ValueError(
"Unsupported Base64 content type. Only image and PDF inputs are "
"supported."
)
return kind
def decode_input_bytes(value: str, kind: InputKind) -> bytes:
if kind is InputKind.ABSOLUTE_PATH:
return resolve_absolute_path(value).read_bytes()
payload = extract_base64_payload(value)
return decode_base64_payload(payload)
@contextmanager
def materialize_bytes_as_temp_file(
data: bytes,
*,
file_type: str,
) -> Generator[Path, None, None]:
suffix = _SUFFIX_BY_FILE_TYPE.get(file_type, ".bin")
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
temp_path = Path(temp_file.name)
try:
temp_file.write(data)
temp_file.close()
yield temp_path
finally:
temp_path.unlink(missing_ok=True)