PaddleOCR-VL-1.6_Demo/ocr_app/cli.py

131 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Path-first unified CLI: one file or one directory, routed by suffix."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from .commands import run_input, run_verify
from .logging_utils import default_log_path, setup_run_logger
from .runtime import PipelineProvider, RuntimeConfig
PROJECT_ROOT = Path(__file__).resolve().parent.parent
LEGACY_COMMANDS = {"image", "pdf", "batch"}
def _add_device_options(parser: argparse.ArgumentParser, default: str | None) -> None:
parser.add_argument("--device", choices=("cpu", "gpu"), default=default or "cpu", help="运行设备")
parser.add_argument("--device-id", type=int, default=0, help="GPU 编号")
parser.add_argument("--threads", type=int, default=None, help="CPU 线程数")
parser.add_argument("--log-file", type=Path, default=None, help="日志文件路径")
parser.add_argument("--verbose", action="store_true", help="输出详细日志")
def build_input_parser(device_override: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="ocr.py",
description="自动按文件后缀处理图片/PDF输入目录时批量使用同一路由逻辑",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("input", type=Path, help="图片、PDF 或目录")
parser.add_argument("--recursive", action="store_true", help="目录模式递归扫描子目录")
parser.add_argument(
"--only",
choices=("all", "image", "pdf"),
default="all",
help="目录模式只处理指定类型",
)
parser.add_argument("--output", type=Path, default=PROJECT_ROOT / "outputs", help="统一输出根目录")
parser.add_argument("--fail-fast", action="store_true", help="单文件失败后立即停止目录任务")
# Image options
parser.add_argument("--warmup", type=int, default=0, help="首张图片预热轮数")
parser.add_argument("--rounds", type=int, default=1, help="每张图片推理轮数")
parser.add_argument("--benchmark-json", type=Path, default=None, help="单图片 Benchmark JSON 路径")
parser.add_argument("--no-result", action="store_true", help="不记录图片 OCR 文本块")
# PDF options
parser.add_argument(
"--pdf-mode",
"--mode",
dest="pdf_mode",
choices=("hybrid", "text", "ocr"),
default="hybrid",
help="PDF 处理模式",
)
parser.add_argument("--pages", help="PDF 页码范围,例如 1-5,8,10-")
parser.add_argument("--dpi", type=int, default=144, help="PDF OCR 页面渲染 DPI")
parser.add_argument("--password", help="PDF 密码")
parser.add_argument("--resume", action="store_true", help="PDF 断点续传")
parser.add_argument("--overwrite", action="store_true", help="覆盖已有 PDF 输出")
parser.add_argument("--keep-rendered", action="store_true", help="保留 OCR 页面 PNG")
parser.add_argument("--max-new-tokens", type=int, default=None, help="VLM 最大生成 token")
parser.add_argument("--min-pixels", type=int, default=None, help="VLM 最小输入像素")
parser.add_argument("--max-pixels", type=int, default=None, help="VLM 最大输入像素")
parser.add_argument("--text-min-chars", type=int, default=50, help="有效文本最小字符数")
parser.add_argument("--text-min-printable-ratio", type=float, default=0.85, help="可打印字符比例阈值")
parser.add_argument("--text-min-content-ratio", type=float, default=0.60, help="字母/数字/CJK 比例阈值")
parser.add_argument("--text-max-replacement-ratio", type=float, default=0.02, help="替换字符最大比例")
parser.add_argument("--text-min-density", type=float, default=25.0, help="文本密度阈值")
_add_device_options(parser, device_override)
return parser
def build_verify_parser(device_override: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="ocr.py verify",
description="验证 CPU/GPU Paddle 环境",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
_add_device_options(parser, device_override)
return parser
def normalize_argv(argv: list[str]) -> tuple[str, list[str]]:
"""Keep old image/pdf/batch prefixes as compatibility aliases."""
if argv and argv[0] == "verify":
return "verify", argv[1:]
if argv and argv[0] in LEGACY_COMMANDS:
return "input", argv[1:]
return "input", argv
def main(device: str | None = None, argv: list[str] | None = None) -> int:
raw_argv = list(sys.argv[1:] if argv is None else argv)
command, normalized = normalize_argv(raw_argv)
parser = build_verify_parser(device) if command == "verify" else build_input_parser(device)
args = parser.parse_args(normalized)
if device is not None:
args.device = device
if command == "verify":
stem = "verify"
category = "verify"
else:
stem = args.input.stem or args.input.name or "input"
category = "input"
log_file = args.log_file or default_log_path(PROJECT_ROOT, category, stem, device=args.device)
logger = setup_run_logger(f"ocr.{category}.{args.device}", log_file, verbose=args.verbose)
logger.info(
"COMMAND_STARTED command=%s input=%s device=%s log=%s",
command,
getattr(args, "input", None),
args.device,
log_file,
)
provider = PipelineProvider(
RuntimeConfig(device=args.device, threads=args.threads, device_id=args.device_id),
logger,
)
try:
if command == "verify":
return run_verify(args, provider, logger, PROJECT_ROOT)
if args.warmup < 0 or args.rounds < 1:
raise ValueError("--warmup 必须 >= 0--rounds 必须 >= 1")
return run_input(args, provider, logger, PROJECT_ROOT)
except Exception as exc:
logger.exception("COMMAND_FAILED command=%s error=%s", command, exc)
return 1