# 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)