Files
Rander 74bdc41608 feat: add multi-language API SDK (Python/Go/TypeScript) (#18049)
* feat: add multi-language API SDK for PaddleOCR async API

Add Python/Go/TypeScript SDKs that wrap the PaddleOCR async job API
(submit → poll → fetch result) into simple blocking interfaces.

Python SDK:
- Integrated into paddleocr/_api_client/ as internal subpackage
- APIClient (sync) + AsyncAPIClient (asyncio) dual interfaces
- CLI subcommand: paddleocr api --model_type ocr/doc_parsing
- Supports PP-OCRv5, PP-StructureV3, PaddleOCR-VL, PaddleOCR-VL-1.5

Go SDK (api_sdk/go/):
- Zero dependencies (stdlib only)
- Functional options pattern + context.Context cancellation
- Operation object with Wait(ctx)/Poll(ctx) methods

TypeScript SDK (api_sdk/typescript/):
- Zero dependencies (native fetch, Node>=18)
- AbortSignal cancellation support
- ESM + CJS dual output

All SDKs share:
- Exponential backoff polling (3s initial, 1.5x, 15s max, configurable timeout)
- Typed error hierarchy (Auth/API/JobFailed/Timeout/Network/FileNotFound)
- URL and local file upload support
- Convenience methods + manual control (submit/wait/poll) interfaces

* fix: address PR review feedback

- Rename PaddleOCRError to PaddleOCRAPIError across all three SDKs
- Replace custom FileNotFoundError with Python builtin in Python SDK
- Expand OCROptions/DocParsingOptions with all API parameters (Python/Go/TS)
- CLI: use Model enum values instead of hardcoded strings
- CLI: allow --model for both ocr and doc_parsing tasks
- Update license header year to 2026
- Simplify TypeScript defaultPayload to empty object

* fix: harden multi-language API SDK

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sdk): jsonl fetch without auth, poll timeout, abort parity

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: harden PaddleOCR official API SDK

Align Python, TypeScript, and Go SDKs around typed jobs, model-aware document parsing, request/poll timeouts, strict errors, resource saving, and integrated release docs for the PaddleOCR official API.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: complete API SDK remediation and default doc parsing to PaddleOCR-VL-1.5

Consolidate SDK hardening, docs, tests, and cross-language default model behavior for document parsing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix bugs

* Fix bugs and optimize sdks

* Fix bugs

* Fix bugs

* Update skills

* Fix bugs

* Update docs

* Fix docs

* Fix doc

* Fix

---------

Co-authored-by: Bobholamovic <bob1998425@hotmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-28 18:24:22 +08:00

310 lines
9.5 KiB
Python

# Copyright (c) 2025 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.
import argparse
import logging
import subprocess
import sys
import time
import warnings
from threading import Thread
import requests
from ._models import (
ChartParsing,
DocImgOrientationClassification,
DocVLM,
FormulaRecognition,
LayoutDetection,
SealTextDetection,
TableCellsDetection,
TableClassification,
TableStructureRecognition,
TextDetection,
TextImageUnwarping,
TextLineOrientationClassification,
TextRecognition,
)
from ._pipelines import (
DocPreprocessor,
DocUnderstanding,
FormulaRecognitionPipeline,
PaddleOCR,
PaddleOCRVL,
PPChatOCRv4Doc,
PPDocTranslation,
PPStructureV3,
SealRecognition,
TableRecognitionPipelineV2,
)
from ._version import version
from ._utils.deprecation import CLIDeprecationWarning
from ._utils.logging import logger
def _register_pipelines(subparsers):
for cls in [
DocPreprocessor,
DocUnderstanding,
FormulaRecognitionPipeline,
PaddleOCR,
PaddleOCRVL,
PPChatOCRv4Doc,
PPDocTranslation,
PPStructureV3,
SealRecognition,
TableRecognitionPipelineV2,
]:
subcommand_executor = cls.get_cli_subcommand_executor()
subparser = subcommand_executor.add_subparser(subparsers)
subparser.set_defaults(executor=subcommand_executor.execute_with_args)
def _register_models(subparsers):
for cls in [
ChartParsing,
DocImgOrientationClassification,
DocVLM,
FormulaRecognition,
LayoutDetection,
SealTextDetection,
TableCellsDetection,
TableClassification,
TableStructureRecognition,
TextDetection,
TextImageUnwarping,
TextLineOrientationClassification,
TextRecognition,
]:
subcommand_executor = cls.get_cli_subcommand_executor()
subparser = subcommand_executor.add_subparser(subparsers)
subparser.set_defaults(executor=subcommand_executor.execute_with_args)
def _register_install_hpi_deps_command(subparsers):
def _install_hpi_deps(args):
hpip = f"hpi-{args.variant}"
try:
subprocess.check_call(["paddlex", "--install", hpip])
subprocess.check_call(["paddlex", "--install", "paddle2onnx"])
except subprocess.CalledProcessError:
sys.exit("Failed to install dependencies")
subparser = subparsers.add_parser("install_hpi_deps")
subparser.add_argument("variant", type=str, choices=["cpu", "gpu", "npu"])
subparser.set_defaults(executor=_install_hpi_deps)
def _register_install_genai_server_deps_command(subparsers):
def _install_genai_server_deps(args):
try:
subprocess.check_call(
["paddlex", "--install", f"genai-{args.variant}-server"]
)
except subprocess.CalledProcessError:
sys.exit("Failed to install dependencies")
subparser = subparsers.add_parser("install_genai_server_deps")
subparser.add_argument(
"variant", type=str, choices=["vllm", "sglang", "fastdeploy"]
)
subparser.set_defaults(executor=_install_genai_server_deps)
def _register_genai_server_command(subparsers):
# TODO: Register the subparser whether the plugin is installed or not
try:
from paddlex.inference.genai.server import get_arg_parser, run_genai_server
except RuntimeError:
return
def _show_prompt_when_server_is_running(host, port, backend):
if host == "0.0.0.0":
host = "localhost"
while True:
try:
resp = requests.get(f"http://{host}:{port}/health", timeout=1)
if resp.status_code == 200:
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
pass
time.sleep(1)
prompt = f"""The PaddleOCR GenAI server has been started. You can either:
1. Set the server URL in the module or pipeline configuration and call the PaddleOCR CLI or Python API. For example:
paddleocr doc_parser --input demo.png --vl_rec_backend {backend}-server --vl_rec_server_url http://{host}:{port}/v1
2. Make HTTP requests directly, or using the OpenAI client library."""
logger.info(prompt)
def _run_genai_server(args):
Thread(
target=_show_prompt_when_server_is_running,
args=(args.host, args.port, args.backend),
daemon=True,
).start()
try:
run_genai_server(args)
except subprocess.CalledProcessError:
sys.exit("Failed to run the server")
paddlex_parser = get_arg_parser()
subparser = subparsers.add_parser(
"genai_server", parents=[paddlex_parser], conflict_handler="resolve"
)
subparser.set_defaults(executor=_run_genai_server)
def _register_doc2md_command(subparsers):
"""Register the doc2md subcommand."""
def _execute_doc2md(args):
if args.formats:
from ._doc2md import supported_formats
fmts = supported_formats()
print("Supported formats: " + ", ".join(f".{f}" for f in fmts))
return
if not args.input:
logger.error("--input is required when --formats is not set")
sys.exit(2)
from ._doc2md import convert
from pathlib import Path
output = args.output
quiet = args.quiet
# Build converter kwargs from CLI args
converter_kwargs = {}
if args.no_drawings:
converter_kwargs["extract_drawings"] = False
if args.no_headers_footers:
converter_kwargs["extract_headers_footers"] = False
if args.sheet_name is not None:
converter_kwargs["sheet_name"] = args.sheet_name
if args.max_rows is not None:
converter_kwargs["max_rows"] = args.max_rows
t1 = time.time()
try:
result = convert(args.input, output=output, **converter_kwargs)
except Exception as e:
logger.error(f"Conversion failed: {e}")
sys.exit(1)
elapsed = (time.time() - t1) * 1000
if not quiet:
logger.info(f"Conversion done in {elapsed:.0f} ms")
if output:
if not quiet:
logger.info(f"Saved to: {output}")
if result.images:
logger.info(f"Images saved to: {Path(output).parent / 'images'}/")
else:
print(result.markdown)
subparser = subparsers.add_parser(
"doc2md",
help="Convert office documents (docx/xlsx/pptx) to Markdown",
)
subparser.add_argument(
"-i",
"--input",
type=str,
default=None,
help="Input file path (.docx/.xlsx/.pptx)",
)
subparser.add_argument(
"-o",
"--output",
type=str,
default=None,
help="Output Markdown file path (prints to stdout if omitted)",
)
subparser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Suppress informational output",
)
subparser.add_argument(
"--formats",
action="store_true",
help="List supported formats and exit",
)
# docx options
subparser.add_argument(
"--no-drawings",
action="store_true",
help="[docx/xlsx] Skip text box / drawing layer content extraction",
)
subparser.add_argument(
"--no-headers-footers",
action="store_true",
help="[docx] Skip header and footer content extraction",
)
# xlsx options
subparser.add_argument(
"--sheet-name",
type=str,
default=None,
help="[xlsx] Convert only the specified sheet (by name)",
)
subparser.add_argument(
"--max-rows",
type=int,
default=None,
help="[xlsx] Maximum number of rows to convert per sheet",
)
subparser.set_defaults(executor=_execute_doc2md)
def _register_api_command(subparsers):
from ._api_client.cli import register_api_command
register_api_command(subparsers)
def _get_parser():
parser = argparse.ArgumentParser(prog="paddleocr")
parser.add_argument(
"-v", "--version", action="version", version=f"%(prog)s {version}"
)
subparsers = parser.add_subparsers(dest="subcommand", metavar="COMMAND")
_register_pipelines(subparsers)
_register_models(subparsers)
_register_install_hpi_deps_command(subparsers)
_register_install_genai_server_deps_command(subparsers)
_register_genai_server_command(subparsers)
_register_doc2md_command(subparsers)
_register_api_command(subparsers)
return parser
def _execute(args):
args.executor(args)
def main():
logger.setLevel(logging.INFO)
warnings.filterwarnings("default", category=CLIDeprecationWarning)
parser = _get_parser()
args = parser.parse_args()
if args.subcommand is None:
parser.print_usage(sys.stderr)
sys.exit(2)
_execute(args)