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
154 lines
4.3 KiB
Python
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)
|