chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 11:59:26 +08:00
commit e904b667c6
2490 changed files with 596352 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# PaddleOCR
name: 🐛 Bug Report
description: Problems with PaddleOCR
body:
- type: markdown
attributes:
value: |
Thank you for submitting a PaddleOCR 🐛 Bug Report!
- type: checkboxes
attributes:
label: 🔎 Search before asking
description: >
Please search the PaddleOCR [Docs](https://paddlepaddle.github.io/PaddleOCR/), [Issues](https://github.com/PaddlePaddle/PaddleOCR/issues) and [Discussions](https://github.com/PaddlePaddle/PaddleOCR/discussions) to see if a similar bug report already exists.
options:
- label: I have searched the PaddleOCR [Docs](https://paddlepaddle.github.io/PaddleOCR/) and found no similar bug report.
required: true
- label: I have searched the PaddleOCR [Issues](https://github.com/PaddlePaddle/PaddleOCR/issues) and found no similar bug report.
required: true
- label: I have searched the PaddleOCR [Discussions](https://github.com/PaddlePaddle/PaddleOCR/discussions) and found no similar bug report.
required: true
- type: textarea
attributes:
label: 🐛 Bug (问题描述)
description: Provide console output with error messages and/or screenshots of the bug. (请提供详细报错信息或者截图)
placeholder: |
💡 ProTip! Include as much information as possible (screenshots, logs, tracebacks etc.) to receive the most helpful response.
validations:
required: true
- type: textarea
attributes:
label: 🏃‍♂️ Environment (运行环境)
description: Please specify the software and hardware you used to produce the bug. (请给出详细依赖包信息,便于复现问题)
placeholder: |
```bash
OS macOS-13.5.2
Environment Jupyter
Python 3.11.2
PaddleOCR 2.8.1
Install git
RAM 16.00 GB
CPU Apple M2
CUDA None
```
validations:
required: true
- type: textarea
attributes:
label: 🌰 Minimal Reproducible Example (最小可复现问题的Demo)
description: >
When asking a question, people will be better able to provide help if you provide code that they can easily understand and use to **reproduce** the problem.
This is referred to by community members as creating a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). (请务必提供该Demo,这样节省大家时间)
placeholder: |
```bash
# Code to reproduce your issue here
```
validations:
required: true
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 🙏 Q&A
url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/q-a
about: Ask the community for help
- name: 💡 Feature requests and ideas
url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/ideas
about: Share ideas for new features
- name: 🙌 Show and tell
url: https://github.com/PaddlePaddle/PaddleOCR/discussions/categories/show-and-tell
about: Show off something you've made
@@ -0,0 +1,55 @@
name: Detect Docs-Only Change
description: >
Output docs_only=true if every changed file in the current pull_request
matches docs/**, **/*.md, or .github/**. On push or workflow_dispatch,
always output docs_only=false.
outputs:
docs_only:
description: "true if change is docs-only, otherwise false"
value: ${{ steps.compute.outputs.docs_only }}
runs:
using: composite
steps:
- id: compute
shell: bash
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if [ "${GITHUB_EVENT_NAME}" != "pull_request" ]; then
echo "docs_only=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git fetch origin "${GITHUB_BASE_REF}" --depth=1 >/dev/null 2>&1 || true
CHANGED_FILES="$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD")"
# >>> matcher-begin
shopt -s globstar extglob nullglob
is_docs_only() {
local files="$1"
if [ -z "$files" ]; then
echo "false"; return 0
fi
while IFS= read -r f; do
[ -z "$f" ] && continue
case "$f" in
docs/*) ;;
.github/*) ;;
*.md) ;;
*)
# also accept nested */**/*.md via case glob
case "$f" in
*/*.md) ;;
*) echo "false"; return 0 ;;
esac
;;
esac
done <<< "$files"
echo "true"
}
result="$(is_docs_only "${CHANGED_FILES}")"
if [ -n "${GITHUB_OUTPUT:-}" ] && [ "${GITHUB_OUTPUT}" != "/dev/null" ]; then
echo "docs_only=${result}" >> "$GITHUB_OUTPUT"
fi
echo "${result}"
# <<< matcher-end
+13
View File
@@ -0,0 +1,13 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
@@ -0,0 +1,39 @@
name: Build/Publish Develop Docs
on:
push:
branches:
- master
- main
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v5
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts
- name: Check docs GitHub source links
run: python tools/check_docs_github_links.py --repo-slug PaddlePaddle/PaddleOCR --forbidden-ref main --forbidden-ref master
- name: Resolve docs GitHub source refs
run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref "${REF_NAME}"
env:
REF_NAME: ${{ github.ref_name }}
- run: |
git fetch origin gh-pages --depth=1
mike deploy --push --update-aliases main latest
env:
DOCS_EDIT_URI: edit/${{ github.ref_name }}/docs/
@@ -0,0 +1,41 @@
name: Build/Publish Release Docs
on:
push:
tags:
- v*
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v6
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v5
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts
- name: Check docs GitHub source links
run: python tools/check_docs_github_links.py --repo-slug PaddlePaddle/PaddleOCR --forbidden-ref main --forbidden-ref master
- name: Resolve docs GitHub source refs
run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref "${REF_NAME}"
env:
REF_NAME: ${{ github.ref_name }}
- run: |
git fetch origin gh-pages --depth=1
mike deploy --push "${REF_NAME}"
env:
REF_NAME: ${{ github.ref_name }}
DOCS_EDIT_URI: blob/${{ github.ref_name }}/docs/
@@ -0,0 +1,23 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
with:
days-before-issue-stale: 90
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+41
View File
@@ -0,0 +1,41 @@
name: PaddleOCR Code Style Check
# NOTE: Job name `check-code-style` is the required status check context
# configured in branch protection. Do not rename without updating settings.
on:
pull_request: {}
push:
branches: ['main', 'release/*']
jobs:
check-code-style:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
fetch-depth: 0
- id: detect
uses: ./.github/actions/detect-docs-only
- uses: actions/setup-python@v6
if: steps.detect.outputs.docs_only != 'true'
with:
python-version: '3.10'
- name: Cache Python dependencies
if: steps.detect.outputs.docs_only != 'true'
uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install Dependencies for Python
if: steps.detect.outputs.docs_only != 'true'
run: |
python -m pip install --upgrade pip
pip install "clang-format==13.0.0"
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
if: steps.detect.outputs.docs_only != 'true'
with:
extra_args: '--all-files'
+39
View File
@@ -0,0 +1,39 @@
name: Docs Anchor Link Check
on:
pull_request:
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'mkdocs-ci.yml'
- 'overrides/**'
jobs:
check-anchor-links:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-mkdocs-${{ hashFiles('mkdocs.yml') }}
restore-keys: |
${{ runner.os }}-pip-mkdocs-
- name: Install dependencies
run: pip install mike mkdocs-material jieba mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 mkdocs-static-i18n markdown-callouts
- name: Resolve docs GitHub source refs
run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref main
- name: Check for broken anchor links
env:
ENABLE_GIT_PLUGINS: 'false'
run: mkdocs build -f mkdocs-ci.yml
+42
View File
@@ -0,0 +1,42 @@
name: Link Checker
on:
repository_dispatch:
workflow_dispatch:
# push:
# branches:
# - main
# schedule:
# - cron: "00 18 * * 6"
jobs:
linkChecker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Resolve docs GitHub source refs
run: python tools/resolve_doc_github_refs.py --placeholder '{{PADDLEOCR_GITHUB_REF}}' --source-ref main
- name: Link Checker
id: lychee
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
args: --exclude 'docs/index/*.md' 'docs/update/*.md' --verbose --no-progress --max-redirects 8 'docs/**/*.md'
format: markdown
fail: false
output: lychee/results.md
- name: Create Issue From File
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6
with:
title: Link Checker Report
content-filepath: ./lychee/results.md
labels: report, automated issue
+39
View File
@@ -0,0 +1,39 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build==1.2.2
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
+117
View File
@@ -0,0 +1,117 @@
name: PaddleOCR PR Tests GPU
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
env:
PR_ID: ${{ github.event.pull_request.number }}
COMMIT_ID: ${{ github.event.pull_request.head.sha || github.sha }}
work_dir: /workspace/PaddleOCR
PADDLENLP_ROOT: /workspace/PaddleOCR
TASK: paddleocr-CI-${{ github.event.pull_request.number }}
BRANCH: ${{ github.event.pull_request.base.ref }}
AGILE_COMPILE_BRANCH: ${{ github.event.pull_request.base.ref }}
DIR_NAME: ${{ github.repository }}
permissions:
contents: read
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: detect
uses: ./.github/actions/detect-docs-only
test-pr-gpu-impl:
runs-on: [self-hosted, GPU-2Card-OCR]
needs: detect-changes
if: needs.detect-changes.outputs.docs_only != 'true'
steps:
- name: run test
env:
py_version: "3.10"
paddle_whl: https://paddle-qa.bj.bcebos.com/paddle-pipeline/Develop-GpuSome-LinuxCentos-Gcc82-Cuda118-Cudnn86-Trt85-Py310-CINN-Compile/latest/paddlepaddle_gpu-0.0.0-cp310-cp310-linux_x86_64.whl
docker_image: ccr-2vdh3abv-pub.cnc.bj.baidubce.com/paddlepaddle/paddle:latest-dev-cuda11.8-cudnn8.6-trt8.5-gcc82
run: |
work_dir=$RANDOM
mkdir $work_dir
cd $work_dir
git clone --depth=1 https://github.com/PaddlePaddle/PaddleOCR.git -b main
cd PaddleOCR
if [ -n "${PR_ID}" ]; then
git fetch origin pull/${PR_ID}/head:ci_build
git checkout ci_build
else
git fetch --depth=1 origin "${COMMIT_ID}"
git checkout "${COMMIT_ID}"
fi
docker run --gpus all --rm -i --name PaddleOCR_CI_$RANDOM \
--shm-size=128g --net=host \
-v $PWD:/workspace -w /workspace \
-e "py_version=${py_version}" \
-e "paddle_whl=${paddle_whl}" \
${docker_image} /bin/bash -c '
ldconfig;
nvidia-smi
df -hl
echo ${py_version}
rm -rf run_env
mkdir run_env
ln -s $(which python${py_version}) run_env/python
ln -s $(which python${py_version}) run_env/python3
ln -s $(which pip${py_version}) run_env/pip
export PATH=$PWD/run_env:${PATH}
git config --global --add safe.directory /workspace
python -m pip install paddlepaddle-gpu==3.1.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/
python -c "import paddle; paddle.version.show()"
python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
python -m pip install pytest
if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi
PADDLEX_SERIES=$(grep -oE "paddlex\[[^]]+\]>=[0-9]+\.[0-9]+" pyproject.toml | head -1 | grep -oE "[0-9]+\.[0-9]+")
if [ -z "$PADDLEX_SERIES" ]; then
echo "Failed to determine PaddleX version requirement from pyproject.toml" >&2
exit 1
fi
PADDLEX_BRANCH="release/${PADDLEX_SERIES}"
echo "Installing PaddleX from branch: ${PADDLEX_BRANCH}"
python -m pip install -e ".[all]" "paddlex@git+https://github.com/PaddlePaddle/PaddleX.git@${PADDLEX_BRANCH}"
python -c "import paddlex; print(f\"Installed paddlex version: {paddlex.__version__}\")"
python -m pytest --verbose tests/
'
# Aggregator: produces a single check named `test-pr-gpu` so branch
# protection (which requires the literal context `test-pr-gpu`) is satisfied
# even when the heavy GPU job is skipped (e.g. docs-only changes). Without
# this, a SKIPPED conclusion on the GPU job leaves the required check
# unsatisfied and blocks merge.
test-pr-gpu:
runs-on: ubuntu-latest
needs: [detect-changes, test-pr-gpu-impl]
if: always()
steps:
- name: Verify required jobs
run: |
if [ "${{ needs.detect-changes.result }}" != "success" ]; then
echo "detect-changes did not succeed: ${{ needs.detect-changes.result }}"
exit 1
fi
if [ "${{ needs.detect-changes.outputs.docs_only }}" = "true" ]; then
echo "Docs-only change; treating GPU tests as not required."
exit 0
fi
if [ "${{ needs.test-pr-gpu-impl.result }}" != "success" ]; then
echo "test-pr-gpu-impl concluded with: ${{ needs.test-pr-gpu-impl.result }}"
exit 1
fi
echo "GPU tests passed."
+108
View File
@@ -0,0 +1,108 @@
name: PaddleOCR PR Tests
on:
push:
branches: ["main", "release/*"]
pull_request:
branches: ["main", "release/*"]
permissions:
contents: read
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.detect.outputs.docs_only }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: detect
uses: ./.github/actions/detect-docs-only
test-pr-python:
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.docs_only != 'true'
strategy:
matrix:
python-version: ["3.8", "3.9", "3.13"]
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Cache dependencies
uses: actions/cache@v5
with:
path: |
~/.cache/pip
~/.local/lib/python${{ matrix.python-version }}/site-packages
~/.paddleocr/
key: ${{ runner.os }}-dependencies-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt', 'pyproject.toml') }}
restore-keys: |
${{ runner.os }}-dependencies-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
# On py3.8 several paddlex transitive deps require py3.9+, so only
# paddleocr[doc2md] is installable. See installation.md for the
# supported extras-by-Python-version matrix.
if [[ "${{ matrix.python-version }}" == "3.8" ]]; then
python -m pip install paddlepaddle==3.0.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/
PADDLEOCR_EXTRAS="[doc2md]"
else
python -m pip install paddlepaddle==3.1.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/
PADDLEOCR_EXTRAS="[all]"
fi
PADDLEX_SERIES=$(grep -oE 'paddlex\[[^]]+\]>=[0-9]+\.[0-9]+' pyproject.toml | head -1 | grep -oE '[0-9]+\.[0-9]+')
if [ -z "$PADDLEX_SERIES" ]; then
echo "Failed to determine PaddleX version requirement from pyproject.toml" >&2
exit 1
fi
python -m pip install "paddlex>=3.7.0,<3.8.0"
PADDLEX_BRANCH="release/${PADDLEX_SERIES}"
echo "Installing PaddleX from branch: ${PADDLEX_BRANCH}"
python -m pip install -e ".${PADDLEOCR_EXTRAS}" "paddlex@git+https://github.com/PaddlePaddle/PaddleX.git@${PADDLEX_BRANCH}"
python -c "import paddlex; print(f'Installed paddlex version: {paddlex.__version__}')"
- name: Test with pytest
run: |
# Skip py38_incompatible tests on py3.8.
if [[ "${{ matrix.python-version }}" == "3.8" ]]; then
pytest --verbose tests/ -m "not resource_intensive and not py38_incompatible"
else
pytest --verbose tests/
fi
# Aggregator: produces a single check named `test-pr` so branch protection
# (which requires the literal context `test-pr`) is satisfied. Without this,
# the matrix above only emits `test-pr (3.x)` checks and the required
# `test-pr` context never reports.
test-pr:
runs-on: ubuntu-latest
needs: [detect-changes, test-pr-python]
if: always()
steps:
- name: Verify required jobs
run: |
if [ "${{ needs.detect-changes.result }}" != "success" ]; then
echo "detect-changes did not succeed: ${{ needs.detect-changes.result }}"
exit 1
fi
if [ "${{ needs.detect-changes.outputs.docs_only }}" = "true" ]; then
echo "Docs-only change; skipping python tests."
exit 0
fi
if [ "${{ needs.test-pr-python.result }}" != "success" ]; then
echo "test-pr-python concluded with: ${{ needs.test-pr-python.result }}"
exit 1
fi
echo "All python matrix variants passed."
+40
View File
@@ -0,0 +1,40 @@
# Byte-compiled / optimized / DLL files
__pycache__/
.ipynb_checkpoints/
*.py[cod]
*$py.class
# C extensions
*.so
/inference/
/inference_results/
/output/
/train_data/
/log/
*.DS_Store
*.vs
*.user
*~
*.vscode
*.idea
*.log
.clang-format
.clang_format.hook
build/
dist/
*.egg-info/
/deploy/android_demo/app/OpenCV/
/deploy/android_demo/app/PaddleLite/
/deploy/android_demo/app/.cxx/
/deploy/android_demo/app/cache/
test_tipc/web/models/
test_tipc/web/node_modules/
.venv/
.worktrees/
+5
View File
@@ -0,0 +1,5 @@
zhuanlan.zhihu.com/*
https://demo.doctrp.top/
http://127.0.0.1:8001/
http://localhost:9003
https://rrc.cvc.uab.es/
+46
View File
@@ -0,0 +1,46 @@
exclude: ^(langchain-paddleocr/|paddleocr-js/)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ['--maxkb=512']
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
- id: end-of-file-fixer
- id: trailing-whitespace
files: \.(c|cc|cxx|cpp|cu|h|hpp|hxx|py)$
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
- id: remove-crlf
- id: remove-tabs
files: \.(c|cc|cxx|cpp|cu|h|hpp|hxx|py)$
- repo: local
hooks:
- id: clang-format
name: clang-format
description: Format files with ClangFormat
entry: bash .clang_format.hook -i
language: system
files: \.(c|cc|cxx|cpp|cu|h|hpp|hxx|cuh|proto)$
# For Python files
- repo: https://github.com/psf/black.git
rev: 24.10.0
hooks:
- id: black
files: (.*\.(py|pyi|bzl)|BUILD|.*\.BUILD|WORKSPACE)$
# Flake8
- repo: https://github.com/pycqa/flake8
rev: 7.1.1
hooks:
- id: flake8
args:
- --count
- --select=E9,F63,F7,F82,E721
- --show-source
- --statistics
exclude: ^benchmark/|^test_tipc/
+3
View File
@@ -0,0 +1,3 @@
[style]
based_on_style = pep8
column_limit = 80
+1
View File
@@ -0,0 +1 @@
www.paddleocr.ai
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (c) 2016 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.
+22
View File
@@ -0,0 +1,22 @@
prune .github
prune applications
prune benchmark
prune configs
prune deploy
prune doc
prune docs
prune overrides
prune ppocr/ext_op
prune ppocr/losses
prune ppocr/metrics
prune ppocr/modeling
prune ppocr/optimizer
prune ppstructure/docs
prune test_tipc
prune tests
exclude .clang_format.hook
exclude .gitignore
exclude .pre-commit-config.yaml
exclude .style.yapf
exclude mkdocs.yml
exclude train.sh
+316
View File
@@ -0,0 +1,316 @@
<div align="center">
<p>
<img width="800" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/Banner.png" alt="Star-history">
</p>
<h3>Global Leading OCR Toolkit & Document AI Engine</h3>
English | [简体中文](./readme/README_cn.md) | [繁體中文](./readme/README_tcn.md) | [日本語](./readme/README_ja.md) | [한국어](./readme/README_ko.md) | [Français](./readme/README_fr.md) | [Русский](./readme/README_ru.md) | [Español](./readme/README_es.md) | [العربية](./readme/README_ar.md)
<!-- icon -->
[![PyPI Downloads](https://static.pepy.tech/badge/paddleocr)](https://pepy.tech/projects/paddleocr)
[![Used by](https://img.shields.io/badge/Used%20by-6k%2B%20repositories-blue)](https://github.com/PaddlePaddle/PaddleOCR/network/dependents)
![python](https://img.shields.io/badge/python-3.8~3.12-aff.svg)
![os](https://img.shields.io/badge/os-linux%2C%20win%2C%20mac-pink.svg)
![hardware](https://img.shields.io/badge/hardware-cpu%2C%20gpu%2C%20xpu%2C%20npu-yellow.svg)
[![AI Studio](https://img.shields.io/badge/PaddleOCR-_Offiical_Website-1927BA?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAMAAADDpiTIAAAABlBMVEU2P+X///+1KuUwAAAHKklEQVR42u3dS5bjOAwEwALvf2fMavZum6IAImI7b2yYSqU+1Zb//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKCR/+fzly7rD92yVg69xh8zeLwOa5w+ZvFYHtc4ft3ykB++cOm79PAp6YO2z/Ngl4ZO5l+9+yT4QAvLqS748VF33Ylzdvzpl72f6z53YIGJ6SZdPeNHcIwOycaADdLgCSIgAIgCOAACAAykIAEAAEAAFAABCAT+WQuQVgeBqXhXQIQAAYegowLQBpbg3gZGFyAC6vgBQAMREA2/YfDPxyaDQNyTNz+3Zwn5J4ZG7PB2h0kHhi7plPCImmJwkPzO0RMa3OET0i5uGlzHFze0xcu0vE2Dq3J4U2vEPgSaHbFzPNDQAAAAAAAMBNovdw+cP/ny+uaf7w/+eYADy8kE+F4Offdjn6zZXhAXgiA78G4MNNsmnu1Xr7b3mbOL8T5Ja5bw/A35EC2LiWpzt1y9jRugBy30fLg3NvHPvnuZcC2NsCUXA/aRmA89V07Fwgt37uH8deCmBr6N44pP4UgaUATpdA7v/cMbIB8okliY65/SW5HhJ1ehPmM+8edwXgpbu4R88FayR32Y/P7oZZbOx13/Zr//ZHx27bAPnkFoyewYlbAhD3TvBobr95gaUAtr1EdNx1lgI4OcTTuR3z6+FZMEDRcu9ZCuDgGCdyGxMa4EgBRMvcjrkM7NgBZw5c0TwAUWUhZwRXA2xaya65Xa3jO2qYZ8bu2AD5w38tG5V8aZpoGN6Tz0bOfa9bceyWAciTO0jWyO1Tc5cLwJmF/JfPnXVyu3/slgHIg1n79O2O5fZv+1cHV7sC2HYqmUdHysNzX3sVkMcjUK5Gc+dMs28E5bGtm0V3gloBOP9vgZv+4sYn3RUaYFMCol5uN77g6lUApc8pWs69Zn7snS9Z9Q8G0S0AUTVUUTG3A54R1KSvo/diLAv5fKzynZeN6xogC75u93+AtBTA47OlAFSv6qY/vp3DAjD8iv2ZdFYJwKynMhTK1rInPfzaxW81LnvSgFP9KxrATaCLA3DxHpbFX31ZyNm5XRZyXG5bNkAWfP0rcrsUwOgC6NIAzgBcBiqAWwPgLrAGuGBP6jr2sifdfiJ6QQM4Bbw4AK4B3129ZSFn53ZZyA/GyFty27IBFMDFAXAG8PbyLQv5xULGPRl0K3h2AbwcgCZPhs+LD1zLnjS6AN4NwMU/DVFh7LyhASreTbvqrxdr/J4XT4Swz4FrTS+AGJ7bNbwAYkxuWzZAVljHrJfbjb9wviYXwFO/FJ8Vli4vaICsEMFyBbA3tmtsAUS0zG1c/bj4YwsZH2/+Whd0+1Nb+S7IE2sfPw4RL0XmsR8Nqvz7qFngmPHF34EqjP15AAofAkosZKPC/K6FVoeP02Ehi540NG6AK/4pYP3cLgVwXwHkDQ1QcSGb/uF4WwCmfX8u/+4vgLINcMUlQIfcLgXwXAF0+BGkpQDuuJx7/hwgpu//cWVuO3wxJOz/z8297vgYBwaIO3O7Kn+c194578ltywbIgu8fl+Z2lS+APvnLjnOv8hsgSqxjgwL4Ln9LAezaj98tgPzy7ZcC+GQzxrWxXQpgx370dm6/H7v6jaBoso5dY1swAFlwHWvfBf5pxVa93fCtdx64+1dsgCy4joWvAfPX9VoKYMs6Zse9/8Mlvv7LILlhAfKFFdsSutJXAdFkL3qlADJPrXFcXAC5KYaH586jO9mtAch9S3T0GQJ726ZWAE49kjP3rlDJuetdaL/1zeqZY9c7CRz7s0wCUPxienQBnAuAAtAAlxaAAAxfyBQABSAACkAAFIAAKAABUAACMEkKwL170oh7V8ueNLoAjgTAXWAN4BRwcABcA2oABTA4AApAAyiAwQFQABpAAQwOgALQADMWUgCuEmNyu15fSIY3gFPAiwPgFFADKIDBAVAAGkABCIACmBqAUAAaQAHMDUCMWkgBuMWw3K43F5LhDeAU8OIAuAmkARTA4AAoAA2gAARAAUwNgLvAGkABDA6Au8AaoKOJuV0vLSTDG8Ap4MUBcBNIAyiAwQFQABpAAQwOgALQAApAABTA1AC4C6wBOhqb23V+IRneAE4BLw6Aa0ANoAAGB0ABaAAFMDgACkADKAABUABTA+AusAboKATAQs4trjV+IYcfuJYCcA6gAATAQk69dFkKQANYyLkFcLIBFIDLQAVwawDsSRrAEWBwAJwCagAFMDgACkADKIDBAVAAGkABCIACmBoAzwXWAApgcADsSRrg0iNACoACEADXgAIwdCFTACykALgGFIAfl0kBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPBv/gN+IH8U6YveYgAAAABJRU5ErkJggg==&labelColor=white)](https://www.paddleocr.com)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/PaddlePaddle/PaddleOCR)
[![License](https://img.shields.io/badge/license-Apache_2.0-green)](../LICENSE)
</div>
**PaddleOCR converts PDF documents and images into structured, LLM-ready data (JSON/Markdown) with industry-leading accuracy. With 70k+ Stars and trusted by top-tier projects like Dify, RAGFlow, and Cherry Studio, PaddleOCR is the bedrock for building intelligent RAG and Agentic applications.**
## 🚀 Key Features
### 📄 Intelligent Document Parsing (LLM-Ready)
> *Transforming messy visuals into structured data for the LLM era.*
* **SOTA Document VLM**: Featuring **PaddleOCR-VL-1.6 (0.9B)**, the industry's leading lightweight vision-language model for document parsing. It achieves 96.3% accuracy on OmniDocBench v1.6, leads in text, formula, and table recognition, and shows significantly enhanced capabilities in ancient documents, rare characters, seals, and charts, with structured outputs in **Markdown** and **JSON** formats.
* **Structure-Aware Conversion**: Powered by **PP-StructureV3**, seamlessly convert complex PDFs and images into **Markdown** or **JSON**. Unlike the PaddleOCR-VL series models, it provides more fine-grained coordinate information, including table cell coordinates, text coordinates, and more.
* **Production-Ready Efficiency**: Achieve commercial-grade accuracy with an ultra-small footprint. Outperforms numerous closed-source solutions in public benchmarks while remaining resource-efficient for edge/cloud deployment.
### 🔍 Universal Text Recognition (Scene OCR)
> *The global gold standard for high-speed, multilingual text spotting.*
* **100+ Languages Supported**: Native recognition for a vast global library. **PP-OCRv6** supports 50 languages with a single unified model (Chinese, English, Japanese, and 46 Latin-script languages) — no model switching needed for multilingual documents.
* **Complex Element Mastery**: Beyond standard text recognition, we support **natural scene text spotting** across a wide range of environments, including IDs, street views, books, and industrial components
* **Performance Leap**: PP-OCRv6 achieves **+4.6% detection** and **+5.1% recognition** accuracy over PP-OCRv5, surpassing mainstream Vision-Language Models. 5.2× CPU inference speedup end-to-end.
<div align="center">
<p>
<img width="100%" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/Arch.jpg" alt="PaddleOCR Architecture">
</p>
</div>
### 🛠️ Developer-Centric Ecosystem
* **Seamless Integration**: The premier choice for the AI Agent ecosystem—deeply integrated with **Dify, RAGFlow, Pathway, and Cherry Studio**.
* **LLM Data Flywheel**: A complete pipeline to build high-quality datasets, providing a sustainable "Data Engine" for fine-tuning Large Language Models.
* **One-Click Deployment**: Supports various hardware backends (NVIDIA GPU, Intel CPU, Kunlunxin XPU, and diverse AI Accelerators).
## 📣 Recent updates
### 🔥 2026.06.11: Release of PaddleOCR 3.7.0
- PP-OCRv6 highlights:
- **Accuracy boost**: Medium tier achieves +4.6% detection and +5.1% recognition over PP-OCRv5_server, surpassing mainstream VLMs (Qwen3-VL-235B, GPT-5.5) with only 34.5M parameters.
- **50 languages unified**: Single model covers Chinese, English, Japanese, and 46 Latin-script languages — no model switching needed.
- **Specialized scenarios**: Major improvements in digital displays, dot-matrix characters, tire prints, and industrial text recognition.
- **Faster inference**: 5.2× CPU speedup (OpenVINO), 6.1× on Apple M4 (tiny), 0.13s on A100 GPU.
- **Three tiers for all scenarios**: tiny (1.5M) / small (7.7M) / medium (34.5M) for edge, mobile, and server deployment.
- **Model availability**: All models are available on [HuggingFace](https://huggingface.co/collections/PaddlePaddle/pp-ocrv6) and [ModelScope](https://www.modelscope.cn/collections/PaddlePaddle/PP-OCRv6).
<details>
<summary><strong>2026.05.28: Release of PaddleOCR 3.6.0</strong></summary>
- PaddleOCR-VL-1.6 highlights:
- **New SOTA Accuracy**: Achieves over 96.3% on OmniDocBench v1.6, also sets new SOTA on OmniDocBench v1.5 and Real5-OmniDocBench, leading both open-source and proprietary solutions in text, formula, and table recognition.
- **Comprehensive Capability Upgrade**: Significant improvements in table, ancient document, and rare character recognition, with notably enhanced seal recognition, spotting, and chart understanding across multiple scenarios.
- **Seamless Migration**: Model architecture is fully consistent with PaddleOCR-VL-1.5, enabling zero-cost adaptation—swap and go.
- **Try it now**: Available on [HuggingFace](https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.6) or our [Official Website](https://www.paddleocr.com).
</details>
<details>
<summary><strong>2026.04.21: Release of PaddleOCR 3.5.0</strong></summary>
* **Flexible inference backends**: Seamlessly switch between Paddle static graph, Paddle dynamic graph, or Transformers. PaddleOCR is now deeply integrated with the Hugging Face ecosystem, and 20 major models support Transformers as the inference backend.
* **Office documents to Markdown**: Convert common document formats such as Word, Excel, and PowerPoint into Markdown.
* **DOCX export for parsed results**: The `PaddleOCR-VL` series, `PP-StructureV3`, and `PP-DocTranslation` now support exporting parsed results to DOCX for convenient viewing and editing in Microsoft Word.
* **Official browser inference SDK**: Released `PaddleOCR.js`, the official browser inference SDK that supports running `PP-OCRv5` directly in the browser.
</details>
<details>
<summary><strong>2026.01.29: Release of PaddleOCR 3.4.0</strong></summary>
* PaddleOCR-VL-1.5 (SOTA 0.9B VLM): Our latest flagship model for document parsing is now live!
* **94.5% Accuracy on OmniDocBench**: Surpassing top-tier general large models and specialized document parsers.
* **Real-World Robustness**: First to introduce the **PP-DocLayoutV3** algorithm for irregular shape positioning, mastering 5 tough scenarios: *Skew, Warping, Scanning, Illumination, and Screen Photography*.
* **Capability Expansion**: Now supports **Seal Recognition**, **Text Spotting**, and expands to **111 languages** (including Chinas Tibetan script and Bengali).
* **Long Document Mastery**: Supports automatic cross-page table merging and hierarchical heading identification.
* **Try it now**: Available on [HuggingFace](https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.5) or our [Official Website](https://www.paddleocr.com).
</details>
<details>
<summary><strong>2025.10.16: Release of PaddleOCR 3.3.0</strong></summary>
- Released PaddleOCR-VL:
- **Model Introduction**:
- **PaddleOCR-VL** is a SOTA and resource-efficient model tailored for document parsing. Its core component is PaddleOCR-VL-0.9B, a compact yet powerful vision-language model (VLM) that integrates a NaViT-style dynamic resolution visual encoder with the ERNIE-4.5-0.3B language model to enable accurate element recognition. **This innovative model efficiently supports 109 languages and excels in recognizing complex elements (e.g., text, tables, formulas, and charts), while maintaining minimal resource consumption**. Through comprehensive evaluations on widely used public benchmarks and in-house benchmarks, PaddleOCR-VL achieves SOTA performance in both page-level document parsing and element-level recognition. It significantly outperforms existing solutions, exhibits strong competitiveness against top-tier VLMs, and delivers fast inference speeds. These strengths make it highly suitable for practical deployment in real-world scenarios. The model has been released on [HuggingFace](https://huggingface.co/PaddlePaddle/PaddleOCR-VL). Everyone is welcome to download and use it! More introduction information can be found in [PaddleOCR-VL](https://www.paddleocr.ai/latest/version3.x/algorithm/PaddleOCR-VL/PaddleOCR-VL.html).
- **Core Features**:
- **Compact yet Powerful VLM Architecture**: We present a novel vision-language model that is specifically designed for resource-efficient inference, achieving outstanding performance in element recognition. By integrating a NaViT-style dynamic high-resolution visual encoder with the lightweight ERNIE-4.5-0.3B language model, we significantly enhance the models recognition capabilities and decoding efficiency. This integration maintains high accuracy while reducing computational demands, making it well-suited for efficient and practical document processing applications.
- **SOTA Performance on Document Parsing**: PaddleOCR-VL achieves state-of-the-art performance in both page-level document parsing and element-level recognition. It significantly outperforms existing pipeline-based solutions and exhibiting strong competitiveness against leading vision-language models (VLMs) in document parsing. Moreover, it excels in recognizing complex document elements, such as text, tables, formulas, and charts, making it suitable for a wide range of challenging content types, including handwritten text and historical documents. This makes it highly versatile and suitable for a wide range of document types and scenarios.
- **Multilingual Support**: PaddleOCR-VL Supports 109 languages, covering major global languages, including but not limited to Chinese, English, Japanese, Latin, and Korean, as well as languages with different scripts and structures, such as Russian (Cyrillic script), Arabic, Hindi (Devanagari script), and Thai. This broad language coverage substantially enhances the applicability of our system to multilingual and globalized document processing scenarios.
- Released PP-OCRv5 Multilingual Recognition Model:
- Improved the accuracy and coverage of Latin script recognition; added support for Cyrillic, Arabic, Devanagari, Telugu, Tamil, and other language systems, covering recognition of 109 languages. The model has only 2M parameters, and the accuracy of some models has increased by over 40% compared to the previous generation.
</details>
<details>
<summary><strong>2025.08.21: Release of PaddleOCR 3.2.0</strong></summary>
- **Significant Model Additions:**
- Introduced training, inference, and deployment for PP-OCRv5 recognition models in English, Thai, and Greek. **The PP-OCRv5 English model delivers an 11% improvement in English scenarios compared to the main PP-OCRv5 model, with the Thai and Greek recognition models achieving accuracies of 82.68% and 89.28%, respectively.**
- **Deployment Capability Upgrades:**
- **Full support for PaddlePaddle framework versions 3.1.0 and 3.1.1.**
- **Comprehensive upgrade of the PP-OCRv5 C++ local deployment solution, now supporting both Linux and Windows, with feature parity and identical accuracy to the Python implementation.**
- **High-performance inference now supports CUDA 12, and inference can be performed using either the Paddle Inference or ONNX Runtime backends.**
- **The high-stability service-oriented deployment solution is now fully open-sourced, allowing users to customize Docker images and SDKs as required.**
- The high-stability service-oriented deployment solution also supports invocation via manually constructed HTTP requests, enabling client-side code development in any programming language.
- **Benchmark Support:**
- **All production lines now support fine-grained benchmarking, enabling measurement of end-to-end inference time as well as per-layer and per-module latency data to assist with performance analysis. [Here's](docs/version3.x/pipeline_usage/instructions/benchmark.en.md) how to set up and use the benchmark feature.**
- **Documentation has been updated to include key metrics for commonly used configurations on mainstream hardware, such as inference latency and memory usage, providing deployment references for users.**
- **Bug Fixes:**
- Resolved the issue of failed log saving during model training.
- Upgraded the data augmentation component for formula models for compatibility with newer versions of the albumentations dependency, and fixed deadlock warnings when using the tokenizers package in multi-process scenarios.
- Fixed inconsistencies in switch behaviors (e.g., `use_chart_parsing`) in the PP-StructureV3 configuration files compared to other pipelines.
- **Other Enhancements:**
- **Separated core and optional dependencies. Only minimal core dependencies are required for basic text recognition; additional dependencies for document parsing and information extraction can be installed as needed.**
- **Enabled support for NVIDIA RTX 50 series graphics cards on Windows; users can refer to the [installation guide](docs/version3.x/installation.en.md) for the corresponding PaddlePaddle framework versions.**
- **PP-OCR series models now support returning single-character coordinates.**
- Added AIStudio, ModelScope, and other model download sources, allowing users to specify the source for model downloads.
- Added support for chart-to-table conversion via the PP-Chart2Table module.
- Optimized documentation descriptions to improve usability.
</details>
[History Log](https://paddlepaddle.github.io/PaddleOCR/latest/en/update/update.html)
## 🚀 Quick Start
### Step 1: Try Online
PaddleOCR official website provides interactive **Experience Center** and **APIs**—no setup required, just one click to experience.
👉 [Visit Official Website](https://www.paddleocr.com)
### Step 2: Local Deployment
For local usage, please refer to the following documentation based on your needs:
- **PP-OCR Series**: See [PP-OCR Documentation](https://www.paddleocr.ai/latest/en/version3.x/pipeline_usage/OCR.html)
- **PaddleOCR-VL Series**: See [PaddleOCR-VL Documentation](https://www.paddleocr.ai/latest/en/version3.x/pipeline_usage/PaddleOCR-VL.html)
- **PP-StructureV3**: See [PP-StructureV3 Documentation](https://www.paddleocr.ai/latest/en/version3.x/pipeline_usage/PP-StructureV3.html)
- **More Capabilities**: See [More Capabilities Documentation](https://www.paddleocr.ai/latest/en/version3.x/pipeline_usage/pipeline_overview.html)
## 🧩 More Features
- Convert models to ONNX format: [Obtaining ONNX Models](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/inference_deployment/others/obtaining_onnx_models.html).
- Accelerate inference using engines like OpenVINO, ONNX Runtime, TensorRT, or perform inference using ONNX format models: [High-Performance Inference](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/inference_deployment/local_inference/high_performance_inference.html).
- Accelerate inference using multi-GPU and multi-process: [Parallel Inference for Pipelines](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/pipeline_usage/instructions/parallel_inference.html).
- Integrate PaddleOCR into applications written in C++, C#, Java, etc.: [Serving](https://paddlepaddle.github.io/PaddleOCR/latest/en/version3.x/inference_deployment/serving/serving.html).
## 🔄 Quick Overview of Execution Results
### PP-OCRv5
<div align="center">
<p>
<img width="100%" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/PP-OCRv5_demo.gif" alt="PP-OCRv5 Demo">
</p>
</div>
### PP-StructureV3
<div align="center">
<p>
<img width="100%" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/PP-StructureV3_demo.gif" alt="PP-StructureV3 Demo">
</p>
</div>
### PaddleOCR-VL
<div align="center">
<p>
<img width="100%" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/PaddleOCR-VL_demo.gif" alt="PP-StructureV3 Demo">
</p>
</div>
## ✨ Stay Tuned
**Star this repository to keep up with exciting updates and new releases, including powerful OCR and document parsing capabilities!**
<div align="center">
<p>
<img width="1200" src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/main/images/paddleocr/README/star_paddleocr2.en.gif" alt="Star-Project">
</p>
</div>
## 👩‍👩‍👧‍👦 Community
<div align="center">
| PaddlePaddle WeChat official account | Join the tech discussion group |
| :---: | :---: |
| <img src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/refs/heads/main/images/paddleocr/README/qrcode_for_paddlepaddle_official_account.jpg" width="150"> | <img src="https://raw.githubusercontent.com/cuicheng01/PaddleX_doc_images/refs/heads/main/images/paddleocr/README/qr_code_for_the_questionnaire.jpg" width="150"> |
</div>
## 😃 Awesome Projects Leveraging PaddleOCR
PaddleOCR wouldn't be where it is today without its incredible community! 💗 A massive thank you to all our longtime partners, new collaborators, and everyone who's poured their passion into PaddleOCR — whether we've named you or not. Your support fuels our fire!
<div align="center">
| Project Name | Description |
| ------------ | ----------- |
| [Dify](https://github.com/langgenius/dify) <a href="https://github.com/langgenius/dify"><img src="https://img.shields.io/github/stars/langgenius/dify"></a>|Production-ready platform for agentic workflow development.|
| [RAGFlow](https://github.com/infiniflow/ragflow) <a href="https://github.com/infiniflow/ragflow"><img src="https://img.shields.io/github/stars/infiniflow/ragflow"></a>|RAG engine based on deep document understanding.|
| [pathway](https://github.com/pathwaycom/pathway) <a href="https://github.com/pathwaycom/pathway"><img src="https://img.shields.io/github/stars/pathwaycom/pathway"></a>|Python ETL framework for stream processing, real-time analytics, LLM pipelines, and RAG.|
| [MinerU](https://github.com/opendatalab/MinerU) <a href="https://github.com/opendatalab/MinerU"><img src="https://img.shields.io/github/stars/opendatalab/MinerU"></a>|Multi-type Document to Markdown Conversion Tool|
| [Umi-OCR](https://github.com/hiroi-sora/Umi-OCR) <a href="https://github.com/hiroi-sora/Umi-OCR"><img src="https://img.shields.io/github/stars/hiroi-sora/Umi-OCR"></a>|Free, Open-source, Batch Offline OCR Software.|
| [cherry-studio](https://github.com/CherryHQ/cherry-studio) <a href="https://github.com/CherryHQ/cherry-studio"><img src="https://img.shields.io/github/stars/CherryHQ/cherry-studio"></a>|A desktop client that supports for multiple LLM providers.|
| [haystack](https://github.com/deepset-ai/haystack)<a href="https://github.com/deepset-ai/haystack"><img src="https://img.shields.io/github/stars/deepset-ai/haystack"></a> |AI orchestration framework to build customizable, production-ready LLM applications.|
| [OmniParser](https://github.com/microsoft/OmniParser)<a href="https://github.com/microsoft/OmniParser"><img src="https://img.shields.io/github/stars/microsoft/OmniParser"></a> |OmniParser: Screen Parsing tool for Pure Vision Based GUI Agent.|
| [QAnything](https://github.com/netease-youdao/QAnything)<a href="https://github.com/netease-youdao/QAnything"><img src="https://img.shields.io/github/stars/netease-youdao/QAnything"></a> |Question and Answer based on Anything.|
| [Learn more projects](./awesome_projects.md) | [More projects based on PaddleOCR](./awesome_projects.md)|
</div>
## 👩‍👩‍👧‍👦 Contributors
<div align="center">
<a href="https://github.com/PaddlePaddle/PaddleOCR/graphs/contributors">
<img src="https://contrib.rocks/image?repo=PaddlePaddle/PaddleOCR&max=400&columns=20" width="800"/>
</a>
</div>
## 🌟 Star
<div align="center">
<p>
<img width="800" src="https://api.star-history.com/svg?repos=PaddlePaddle/PaddleOCR&type=Date" alt="Star-history">
</p>
</div>
## 📄 License
This project is released under the [Apache 2.0 license](LICENSE).
## 🎓 Citation
```bibtex
@misc{cui2025paddleocr30technicalreport,
title={PaddleOCR 3.0 Technical Report},
author={Cheng Cui and Ting Sun and Manhui Lin and Tingquan Gao and Yubo Zhang and Jiaxuan Liu and Xueqing Wang and Zelun Zhang and Changda Zhou and Hongen Liu and Yue Zhang and Wenyu Lv and Kui Huang and Yichao Zhang and Jing Zhang and Jun Zhang and Yi Liu and Dianhai Yu and Yanjun Ma},
year={2025},
eprint={2507.05595},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2507.05595},
}
@misc{cui2025paddleocrvlboostingmultilingualdocument,
title={PaddleOCR-VL: Boosting Multilingual Document Parsing via a 0.9B Ultra-Compact Vision-Language Model},
author={Cheng Cui and Ting Sun and Suyin Liang and Tingquan Gao and Zelun Zhang and Jiaxuan Liu and Xueqing Wang and Changda Zhou and Hongen Liu and Manhui Lin and Yue Zhang and Yubo Zhang and Handong Zheng and Jing Zhang and Jun Zhang and Yi Liu and Dianhai Yu and Yanjun Ma},
year={2025},
eprint={2510.14528},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2510.14528},
}
@misc{cui2026paddleocrvl15multitask09bvlm,
title={PaddleOCR-VL-1.5: Towards a Multi-Task 0.9B VLM for Robust In-the-Wild Document Parsing},
author={Cheng Cui and Ting Sun and Suyin Liang and Tingquan Gao and Zelun Zhang and Jiaxuan Liu and Xueqing Wang and Changda Zhou and Hongen Liu and Manhui Lin and Yue Zhang and Yubo Zhang and Yi Liu and Dianhai Yu and Yanjun Ma},
year={2026},
eprint={2601.21957},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2601.21957},
}
@misc{zhang2026paddleocrvl16expandingfrontierdocument,
title={PaddleOCR-VL-1.6: Expanding the Frontier of Document Parsing with Under-Optimized Region Refinement and Progressive Post-Training},
author={Zelun Zhang and Hongen Liu and Suyin Liang and Yubo Zhang and Yiqing Xiang and Jiaxuan Liu and Ting Sun and Manhui Lin and Yue Zhang and Changda Zhou and Tingquan Gao and Cheng Cui and Yi Liu and Dianhai Yu and Yanjun Ma},
year={2026},
eprint={2606.03264},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2606.03264},
}
```
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`PaddlePaddle/PaddleOCR`
- 原始仓库:https://github.com/PaddlePaddle/PaddleOCR
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+194
View File
@@ -0,0 +1,194 @@
# PaddleOCR Release SOP
[中文](./RELEASING_cn.md)
## Scope
This document describes the standard release process for PaddleOCR.
## Release Types
The current process supports the following two release types:
- `bump patch`: for example, `3.4.0 -> 3.4.1`
- `bump minor`: for example, `3.4.x -> 3.5.0`
For `bump major`:
- The current process does not directly support this scenario
- A major release process should be discussed and designed separately, for example by introducing an additional branch or a new preparation flow
## Release Principles
- Daily development happens on `main`
- Official releases are made only from `release/X.Y`
- Official tags must use `vX.Y.Z`
- Do not create official release tags on `main`
## Standard Release Process
### 1. Confirm the release target
First confirm which release line this release belongs to and what the target version is.
Examples:
- Release `3.4.1` on `release/3.4`
- Release `3.5.0` on `release/3.5`
### 2. Create or switch to the release branch
If the release line does not exist yet, create the corresponding `release/X.Y` branch from `main`.
If it already exists, switch to that branch and continue release preparation there.
Requirements:
- One minor release line corresponds to one fixed `release/X.Y` branch
- That branch should contain only the release content and patch fixes for that line
### 3. Pick release content from main
Based on the release scope, `cherry-pick` the required commits from `main` into `release/X.Y` until the release content is ready.
Requirements:
- Only pick what is needed for the current release
- Avoid bringing unrelated new features into the release branch
- If there are release-specific fixes on the release branch, keep them limited to the current release scope
### 4. Complete pre-release checks
Complete pre-release checks on `release/X.Y`.
At minimum, this should include:
- The current branch is correct
- The working tree is clean
- The version is as expected
- Key functionality is verified
- Required tests, builds, packaging, and regression checks have passed
- Release notes are ready
### 5. Create the official tag
Once the release is ready, create the official tag on `release/X.Y`:
- The tag format must be `vX.Y.Z`
Examples:
- `v3.4.1`
- `v3.5.0`
Requirements:
- The tag must be created on the release branch for the current official release
- Do not use development tags as official release tags
### 6. Publish the GitHub Release
Create a GitHub Release based on the official tag for this release.
### 7. Update dependency constraints and release notes
If this release is the first release of a new minor line, or if it changes PaddleX dependency requirements, update the related release materials before or after the official tag is published.
At minimum, this should include:
- Checking whether the `paddlex` dependency constraints in `pyproject.toml` match the target release
- Checking whether installation docs, upgrade notes, and release notes are aligned with the release version
Completion criteria:
- The `paddlex` dependency constraints match the target release
- The version information in the documentation matches the released version
### 8. Sync the release branch lineage back to main
After the first official release of a new `release/X.Y` line is completed, sync the lineage of that `release/X.Y` branch back to `main`.
This is a fixed step in the current workflow and should be done at least once for each new minor release line.
Purpose:
- Ensure `main` correctly reflects that the release line has produced an official version
- Keep subsequent development and release cadence aligned
Requirements:
- Perform this once after the first official release of each new `release/X.Y`
- For later patch releases on the same `release/X.Y`, it is usually not necessary to repeat it
### 9. Move on to the next development cycle or patch release
After the release:
- `main` continues with ongoing development
- `release/X.Y` continues to maintain that release line
If more patch releases are needed later on the same line:
- Continue preparing patches on `release/X.Y`
- `cherry-pick` from `main` as needed
- Repeat the relevant steps in this SOP
## How to Handle Different Bump Types
### Patch Release
Applicable scenarios:
- Fixing production issues
- Small compatibility fixes
- Documentation, dependency, or stability patches
How to handle it:
- Continue preparing changes on the existing `release/X.Y`
- Create the next patch tag, for example `v3.4.2`
### Minor Release
Applicable scenarios:
- Starting a new release line
- Releasing the next stable version, for example `3.5.0`
How to handle it:
- Create a new `release/X.Y` from `main`
- Prepare the release following the standard process
- Create the first official tag, for example `v3.5.0`
- Update `paddlex` dependency constraints if needed
- Sync the lineage of that release branch back to `main` after the release
### Major Release
Current conclusion:
- It is not included in this SOP for now
- A separate process needs to be discussed and designed later
Before a dedicated solution is defined, do not directly reuse the current minor/patch process for a major release.
## Release Checklist
Before each release, confirm the following:
- The target version and corresponding release branch have been confirmed
- The content on the release branch is ready
- The release scope has been finalized
- Key tests and regression checks have passed
- Release notes are ready
- The official tag uses the `vX.Y.Z` format
- The GitHub Release has been created
- If this is the first official release on `release/X.Y`, its lineage has been synced back to `main`
## Daily Maintenance Recommendations
- New features should go to `main` first
- Official releases should always be performed through `release/X.Y`
- Patch releases should always be maintained on the corresponding `release/X.Y`
- For each new `release/X.Y`, sync its lineage once after the first official release
If the current process changes, this document should be updated accordingly.
+194
View File
@@ -0,0 +1,194 @@
# PaddleOCR Release SOP
[English](./RELEASING.md)
## 适用范围
本文档用于说明 PaddleOCR 的标准发版流程。
## 版本类型
当前流程支持以下两类发布:
- `bump patch`:例如 `3.4.0 -> 3.4.1`
- `bump minor`:例如 `3.4.x -> 3.5.0`
对于 `bump major`
- 当前流程暂不支持直接覆盖这类场景
- 如需发布大版本,需要单独讨论并设计额外流程,例如引入额外分支或新的版本准备方式
## 发版原则
- 日常开发在 `main` 分支进行
- 正式发布只在 `release/X.Y` 分支进行
- 正式标签只使用 `vX.Y.Z`
- 不在 `main` 分支打正式发布标签
## 标准发版流程
### 1. 确认发布目标
先确认本次发布属于哪一条版本线,以及目标版本号。
示例:
- 发布 `3.4.1`,对应分支为 `release/3.4`
- 发布 `3.5.0`,对应分支为 `release/3.5`
### 2. 创建或切换到 release 分支
如果该版本线尚未建立,则从 `main` 创建对应的 `release/X.Y` 分支。
如果该版本线已存在,则直接切换到该分支继续发版准备。
要求:
- 一个 minor 版本线对应一个固定的 `release/X.Y` 分支
- 该分支只承载该版本线的发布内容和补丁修复
### 3. 从 main 挑选发布内容
根据本次发布范围,从 `main` 将需要发布的提交按需 `cherry-pick``release/X.Y`,直到版本内容 ready。
要求:
- 只挑选本次发布需要的内容
- 避免把与本次发布无关的新功能带入 release 分支
- 如果 release 分支上出现专门的发布修复,也应仅限于本次发布范围
### 4. 完成发布前检查
`release/X.Y` 上完成发布前检查。
至少应包括:
- 当前分支正确
- 工作区干净
- 版本号符合预期
- 关键功能验证通过
- 必要的测试、构建、打包和回归通过
- 发布说明已经准备好
### 5. 打正式标签
确认版本 ready 后,在 `release/X.Y` 分支上打正式标签:
- 标签格式必须为 `vX.Y.Z`
示例:
- `v3.4.1`
- `v3.5.0`
要求:
- 标签必须打在本次正式发布对应的 release 分支上
- 不使用开发态标签作为正式发布标签
### 6. 发布 GitHub Release
在 GitHub 上基于本次正式标签创建 Release。
### 7. 更新依赖约束与发布说明
如果本次发布是新的 minor 版本首发,或本次发布涉及 PaddleX 依赖变化,则在正式标签发布前后同步完成相关更新。
至少应包括:
- 检查 `pyproject.toml``paddlex` 相关依赖约束是否与本次发布版本匹配
- 检查安装说明、升级说明和发布说明中的版本信息是否与本次发布一致
完成要求:
- `paddlex` 相关依赖约束与本次发布目标一致
- 文档中的版本信息与本次发布版本一致
### 8. 将 release 分支的 lineage 同步回 main
当一条新的 `release/X.Y` 发布线完成首个正式版本发布后,需要将该 `release/X.Y` 的 lineage 同步回 `main`
这一步是当前流程中的固定步骤,每条新的 minor 发布线至少执行一次。
目的:
-`main` 正确感知该发布线已经完成的正式版本
- 保持后续开发与发布节奏一致
要求:
- 每条新的 `release/X.Y` 在首个正式版本发布后执行一次
- 同一条 `release/X.Y` 后续如果继续发布补丁版本,通常不要求重复执行
### 9. 进入下一轮开发或补丁发布
发布完成后:
- `main` 继续进行后续开发
- `release/X.Y` 继续维护该版本线
如果后续还要发布该版本线的补丁版本:
- 继续在 `release/X.Y` 上准备补丁
-`main` 按需 `cherry-pick`
- 重复执行本 SOP 中的发布步骤
## 不同 bump 类型的处理方式
### Patch 发布
适用场景:
- 修复线上问题
- 小范围兼容性修复
- 文档、依赖或稳定性补丁
处理方式:
- 在现有 `release/X.Y` 分支上继续准备内容
- 打下一个 patch 标签,例如 `v3.4.2`
### Minor 发布
适用场景:
- 开启一条新的发布线
- 发布下一阶段稳定版本,例如 `3.5.0`
处理方式:
-`main` 建立新的 `release/X.Y`
- 按标准流程准备版本内容
- 打首个正式标签,例如 `v3.5.0`
- 如有需要,同步更新 `paddlex` 相关依赖约束
- 发布后将该 release 分支的 lineage 同步回 `main`
### Major 发布
当前结论:
- 暂不纳入本 SOP
- 后续需要单独讨论并设计流程
在新的方案明确之前,不建议直接套用当前 minor/patch 的做法处理 major 发布。
## 发布检查清单
每次发版前,请确认以下事项:
- 已确认目标版本号和对应 release 分支
- release 分支中的内容已经 ready
- 发布范围已经收敛
- 关键测试和回归已通过
- 发布说明已准备完成
- 正式标签格式为 `vX.Y.Z`
- GitHub Release 已创建
- 如本次是该 `release/X.Y` 的首个正式版本,发布完成后已将 lineage 同步回 `main`
## 日常维护建议
- 新功能优先进入 `main`
- 正式发布始终通过 `release/X.Y` 执行
- 补丁版本始终在对应的 `release/X.Y` 上维护
- 每条新的 `release/X.Y` 在首个正式版本发布后执行一次 lineage 同步
如当前流程发生调整,应同步更新本文档。
+71
View File
@@ -0,0 +1,71 @@
# PaddleOCR API SDK 集成测试报告
**测试分支**: feature/api-sdk (PR #18049)
**测试环境**: macOS Darwin 24.3.0, Python 3.9.6, requests 2.32.5
**API Endpoint**: https://paddleocr.aistudio-app.com/api/v2/ocr/jobs
---
## 测试结果总览
| # | 测试项                | 结果  | 耗时 | 说明                       |
| -----| ---------------------------------------| --------| ------| ---------------------------------------------------|
| 1 | OCR URL (PP-OCRv5)          | ✅ PASS | 3.6s | URL 输入,默认参数                |
| 2 | OCR URL + 自定义 Options       | ✅ PASS | 3.6s | 设置 use_doc_orientation_classify=True      |
| 3 | Doc Parsing URL (PP-StructureV3)   | ✅ PASS | 3.7s | 文档版面解析                   |
| 4 | Submit + Poll 分步调用        | ✅ PASS | 3.7s | 非阻塞 APIsubmit → get_result → wait_for_result |
| 5 | OCR 本地文件上传           | ✅ PASS | 4.3s | file_path 模式                  |
| 6 | 错误处理 (无效 token)         | ✅ PASS | 0.2s | 正确抛出 AuthError                |
| 7 | 输入校验               | ✅ PASS | 0.0s | 缺少输入 / 互斥参数均正确拦截           |
| 8 | Context Manager (with)        | ✅ PASS | 3.6s | with 语句正常工作                 |
| 9 | PaddleOCR-VL 模型           | ✅ PASS | 3.6s | VL 模型正常返回 markdown             |
| 10 | PaddleOCR-VL-1.5 模型         | ✅ PASS | 3.6s | VL-1.5 模型正常返回 markdown           |
| 11 | Doc Parsing 文件上传 (PP-StructureV3) | ✅ PASS | 4.3s | 本地文件上传 + 文档解析              |
**总计: 11 passed, 0 failed**
---
## 发现的 Bug(已修复验证)
### 🔴 阻塞性 Bug: `fetch_jsonl` 请求 BOS 签名 URL 时携带了多余的 Authorization header
**文件**: `paddleocr/_api_client/_http.py` (同步) + `async_client.py` (异步)
**现象**: 任务提交和轮询均成功,但在最后一步下载 JSONL 结果文件时,SDK 使用带有 `Authorization: bearer <paddle_token>` 的 session 去请求百度 BOS 对象存储的预签名 URL。BOS 不认识这个 header,返回 400 Bad Request。
**根因**: `HTTPClient.fetch_jsonl()` 使用 `self._session.get(url)` 发起请求,而 session 在初始化时设置了 `Authorization` header。BOS 的预签名 URL 已经包含了自己的鉴权参数(`authorization=bce-auth-v1/...`),额外的 Authorization header 导致冲突。
**修复**: `fetch_jsonl` 应使用不带 auth header 的独立请求:
```python
# 修复前
resp = self._session.get(url, timeout=self._timeout)
# 修复后
resp = requests.get(url, timeout=self._timeout)
```
**影响范围**: 所有实际 API 调用(OCR、Doc Parsing、所有模型)在修复前均无法获取结果。这是一个 **必须在合入前修复的阻塞性 bug**
---
## 其他已知问题(非阻塞,可后续迭代)
| 严重度 | 语言 | 问题 |
|--------|------|------|
| 中 | Python | AsyncAPIClient._poll_until_done 使用硬编码 DEFAULT_MAX_WAIT_TIME,忽略用户设置的 timeout |
| 中 | Go | submitURL/submitFile/getJobStatus 未使用 http.NewRequestWithContextcontext 取消无法中断 HTTP 请求 |
| 中 | TypeScript | poller.ts 的 sleep 方法 abort listener 未设置 `{ once: true }`,长轮询会泄漏 listener |
| 中 | TypeScript | http.ts 的 fetchJsonl 未检查 resp.ok(同样的 BOS auth 问题可能也存在于 TS SDK |
| 低 | 全部 | 轮询循环先 sleep 再 check,对已完成任务多等 3 秒 |
| 低 | Python | CLI argparse 的 store_true 传 False 而非 None,导致多余字段发送给 API |
---
## 结论
**修复 `fetch_jsonl` 的 auth header 问题后,Python SDK 的核心功能全部正常工作。** 4 个模型(PP-OCRv5、PP-StructureV3、PaddleOCR-VL、PaddleOCR-VL-1.5)均可正常调用,URL 输入和文件上传两种模式均可用,错误处理和输入校验逻辑正确。
**建议**:
1. ⚠️ **合入前必须修复** `fetch_jsonl` 的 BOS auth header bugPython 同步 + 异步,以及 Go/TypeScript SDK 中的同类问题)
2. 其他问题可以合入后迭代修复
+50
View File
@@ -0,0 +1,50 @@
# PaddleOCR official API SDKs
English | [简体中文](README_cn.md)
This directory contains source-adjacent maintainer files for the PaddleOCR
official API SDKs. The SDKs call hosted PaddleOCR official API services; they do
not run local PaddleOCR inference or load local models.
The official user documentation:
- [Overview](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/overview.md)
- [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md)
- [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md)
- [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md)
- [CLI](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/cli.md)
## Maintainer Files
| File | Purpose |
| --- | --- |
| [`typescript/README.md`](typescript/README.md) | Package-level README for the TypeScript SDK. |
| [`go/README.md`](go/README.md) | Package-level README for the Go SDK. |
The Python SDK is part of the main `paddleocr` package.
## Package Locations
| Language | Source location | User docs |
| --- | --- | --- |
| Python | [`../paddleocr`](../paddleocr) | [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) |
| TypeScript | [`typescript`](typescript) | [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) |
| Go | [`go`](go) | [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) |
## Validation
Run these from the PaddleOCR repo root directory:
```bash
# Python
python -m pytest tests/api_client/
# TypeScript
cd api_sdk/typescript
npm run lint
npm test
# Go
cd ../go
go test ./...
```
+48
View File
@@ -0,0 +1,48 @@
# PaddleOCR 官方 API SDK
[English](README.md) | 简体中文
本目录包含 PaddleOCR 官方 API SDK 的源码相邻维护文档。SDK 调用 PaddleOCR 官方 API 托管服务;它们不在本地执行 PaddleOCR 推理,也不加载本地模型。
正式用户文档:
- [总览](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/overview.md)
- [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md)
- [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md)
- [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md)
- [CLI](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/cli.md)
## 维护者文件
| 文件 | 作用 |
| --- | --- |
| [`typescript/README_cn.md`](typescript/README_cn.md) | TypeScript SDK 的包级 README。 |
| [`go/README_cn.md`](go/README_cn.md) | Go SDK 的包级 README。 |
Python SDK 是主 `paddleocr` 包的一部分。
## 包位置
| 语言 | 源码位置 | 用户文档 |
| --- | --- | --- |
| Python | [`../paddleocr`](../paddleocr) | [Python SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/python.md) |
| TypeScript | [`typescript`](typescript) | [TypeScript SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md) |
| Go | [`go`](go) | [Go SDK](../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md) |
## 验证
在 PaddleOCR 仓库根目录执行:
```bash
# Python
python -m pytest tests/api_client/
# TypeScript
cd api_sdk/typescript
npm run lint
npm test
# Go
cd ../go
go test ./...
```
+72
View File
@@ -0,0 +1,72 @@
# PaddleOCR Go SDK
English | [简体中文](README_cn.md)
Go client for the PaddleOCR official API. It submits OCR and document parsing
jobs to hosted PaddleOCR services; it does not run local PaddleOCR inference or
load local models.
Official user docs:
- [Go SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md)
- [Go SDK (English)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.en.md)
## Install
```bash
go get github.com/PaddlePaddle/PaddleOCR/api_sdk/go
```
Versioned releases use submodule tags such as `api_sdk/go/v0.1.0`.
## Minimal Usage
Set `PADDLEOCR_ACCESS_TOKEN` or pass `WithToken` when constructing the client:
```bash
export PADDLEOCR_ACCESS_TOKEN="your-access-token"
```
```go
client, err := paddleocr.NewClient()
if err != nil {
return err
}
result, err := client.OCR(ctx, &paddleocr.OCRRequest{
Model: paddleocr.PPOCRv5,
FileURL: "https://example.com/invoice.pdf",
})
if err != nil {
return err
}
fmt.Println(result.JobID, len(result.Pages))
```
Set `Model: paddleocr.PPOCRv6` (or `"PP-OCRv6"`) to use the PP-OCRv6 hosted OCR model.
Set `Model: paddleocr.PPOCRv5Latin` (or `"PP-OCRv5-latin"`) to use the PP-OCRv5 Latin-script hosted OCR model.
Document parsing defaults to PaddleOCR-VL-1.6:
```go
doc, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{
FilePath: "./report.pdf",
Options: &paddleocr.PaddleOCRVLOptions{
UseChartRecognition: paddleocr.Bool(true),
},
})
if err != nil {
return err
}
fmt.Println(doc.JobID, len(doc.Pages))
```
## Build And Test
```bash
go test ./...
go vet ./...
go test -race ./...
```
`go test -race ./...` is recommended before public release.
+70
View File
@@ -0,0 +1,70 @@
# PaddleOCR Go SDK
[English](README.md) | 简体中文
面向 PaddleOCR 官方 API 的 Go 客户端。它会把 OCR 和文档解析任务提交到 PaddleOCR 官方托管服务;不会运行本地 PaddleOCR 推理,也不会加载本地模型。
正式用户文档:
- [Go SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.md)
- [Go SDK(英文)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/go.en.md)
## 安装
```bash
go get github.com/PaddlePaddle/PaddleOCR/api_sdk/go
```
版本化发布使用 `api_sdk/go/v0.1.0` 这类子目录 module tag。
## 最小示例
设置 `PADDLEOCR_ACCESS_TOKEN`,或在构造客户端时传入 `WithToken`
```bash
export PADDLEOCR_ACCESS_TOKEN="your-access-token"
```
```go
client, err := paddleocr.NewClient()
if err != nil {
return err
}
result, err := client.OCR(ctx, &paddleocr.OCRRequest{
Model: paddleocr.PPOCRv5,
FileURL: "https://example.com/invoice.pdf",
})
if err != nil {
return err
}
fmt.Println(result.JobID, len(result.Pages))
```
`Model` 设为 `paddleocr.PPOCRv6`(或 `"PP-OCRv6"`)可使用 PP-OCRv6 云端 OCR 模型。
`Model` 设为 `paddleocr.PPOCRv5Latin`(或 `"PP-OCRv5-latin"`)可使用 PP-OCRv5 拉丁语系云端 OCR 模型。
文档解析默认使用 PaddleOCR-VL-1.6
```go
doc, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{
FilePath: "./report.pdf",
Options: &paddleocr.PaddleOCRVLOptions{
UseChartRecognition: paddleocr.Bool(true),
},
})
if err != nil {
return err
}
fmt.Println(doc.JobID, len(doc.Pages))
```
## 构建与测试
```bash
go test ./...
go vet ./...
go test -race ./...
```
公开发布前建议运行 `go test -race ./...`
+66
View File
@@ -0,0 +1,66 @@
// 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.
package paddleocr
import (
"net/http"
"os"
"strings"
"time"
)
type Client struct {
token string
baseURL string
jobsURL string
requestTimeout time.Duration
pollTimeout time.Duration
clientPlatform string
httpClient *http.Client
}
func NewClient(opts ...ClientOption) (*Client, error) {
c := &Client{
requestTimeout: 5 * time.Minute,
pollTimeout: 10 * time.Minute,
}
for _, opt := range opts {
opt(c)
}
if c.token == "" {
c.token = os.Getenv("PADDLEOCR_ACCESS_TOKEN")
}
if c.token == "" {
return nil, &AuthError{PaddleOCRAPIError{Message: "Token is required. Set PADDLEOCR_ACCESS_TOKEN or use WithToken()."}}
}
if c.baseURL == "" {
c.baseURL = os.Getenv("PADDLEOCR_BASE_URL")
}
if c.baseURL == "" {
c.baseURL = DefaultBaseURL
}
c.baseURL = strings.TrimRight(c.baseURL, "/")
c.jobsURL = c.baseURL + apiPath
if c.httpClient == nil {
c.httpClient = &http.Client{Timeout: c.requestTimeout}
}
return c, nil
}
func (c *Client) setClientPlatformHeader(req *http.Request) {
if c.clientPlatform != "" {
req.Header.Set("Client-Platform", c.clientPlatform)
}
}
+235
View File
@@ -0,0 +1,235 @@
package paddleocr
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestClientPlatformHeader(t *testing.T) {
var got string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = r.Header.Get("Client-Platform")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"data": map[string]string{"jobId": "job-1"},
})
}))
defer server.Close()
client, err := NewClient(
WithToken("token"),
WithBaseURL(server.URL),
WithClientPlatform("my-app"),
)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}
job, err := client.SubmitOCR(context.Background(), &OCRRequest{
FileURL: "https://example.test/input.pdf",
})
if err != nil {
t.Fatalf("SubmitOCR() error = %v", err)
}
if job.JobID != "job-1" {
t.Fatalf("JobID = %q, want job-1", job.JobID)
}
if got != "my-app" {
t.Fatalf("Client-Platform = %q, want my-app", got)
}
}
func TestDocumentParsingOptionsIncludeCurrentAndFutureServiceParameters(t *testing.T) {
trueValue := true
falseValue := false
var got map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
OptionalPayload map[string]interface{} `json:"optionalPayload"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("Decode request body error = %v", err)
}
got = body.OptionalPayload
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"data": map[string]string{"jobId": "job-doc"},
})
}))
defer server.Close()
client, err := NewClient(
WithToken("token"),
WithBaseURL(server.URL),
)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}
_, err = client.SubmitDocumentParsing(context.Background(), &DocParsingRequest{
Model: PaddleOCRVL16,
FileURL: "https://example.test/doc.pdf",
Options: &PaddleOCRVLOptions{
UseOcrForImageBlock: &trueValue,
FormatBlockContent: &trueValue,
MarkdownIgnoreLabels: []string{"image"},
VlmExtraArgs: map[string]interface{}{"temperature": 0.1},
ReturnMarkdownImages: &falseValue,
OutputFormats: []string{"docx"},
ExtraOptions: map[string]interface{}{"futureOption": "enabled"},
},
})
if err != nil {
t.Fatalf("SubmitDocumentParsing() error = %v", err)
}
if got["useOcrForImageBlock"] != true || got["formatBlockContent"] != true {
t.Fatalf("got boolean options %#v", got)
}
if got["returnMarkdownImages"] != false || got["futureOption"] != "enabled" {
t.Fatalf("got passthrough options %#v", got)
}
}
func TestResultParsersPreserveRawFieldsAndDataInfo(t *testing.T) {
ocrLine := map[string]interface{}{
"result": map[string]interface{}{
"dataInfo": map[string]interface{}{"numPages": float64(1)},
"ocrResults": []interface{}{
map[string]interface{}{
"prunedResult": map[string]interface{}{"text": "hello"},
"ocrImage": "ocr.png",
"docPreprocessingImage": "pre.png",
"inputImage": "input.png",
},
},
},
}
ocrResult, err := parseOCRResult("job-ocr", []map[string]interface{}{ocrLine})
if err != nil {
t.Fatalf("parseOCRResult() error = %v", err)
}
if ocrResult.DataInfo["numPages"] != float64(1) {
t.Fatalf("OCR metadata not preserved: %#v", ocrResult)
}
if ocrResult.Pages[0].DocPreprocessingImageURL != "pre.png" || ocrResult.Pages[0].InputImageURL != "input.png" {
t.Fatalf("OCR page image URLs not preserved: %#v", ocrResult.Pages[0])
}
if ocrResult.Pages[0].Raw["ocrImage"] != "ocr.png" {
t.Fatalf("OCR raw page not preserved: %#v", ocrResult.Pages[0].Raw)
}
docPage := map[string]interface{}{
"prunedResult": map[string]interface{}{"blocks": []interface{}{map[string]interface{}{"label": "text"}}},
"markdown": map[string]interface{}{"text": "hello", "images": map[string]interface{}{"figure.png": "figure-url"}, "isStart": true},
"outputImages": map[string]interface{}{"page.png": "page-url"},
"inputImage": "input.png",
"exports": map[string]interface{}{"docx": "docx-url"},
}
docLine := map[string]interface{}{
"result": map[string]interface{}{
"dataInfo": map[string]interface{}{"numPages": float64(1)},
"layoutParsingResults": []interface{}{docPage},
},
}
docResult, err := parseDocParsingResult("job-doc", []map[string]interface{}{docLine})
if err != nil {
t.Fatalf("parseDocParsingResult() error = %v", err)
}
if docResult.DataInfo["numPages"] != float64(1) {
t.Fatalf("document metadata not preserved: %#v", docResult)
}
if docResult.Pages[0].PrunedResult == nil || docResult.Pages[0].Raw["inputImage"] != "input.png" {
t.Fatalf("document page raw fields not preserved: %#v", docResult.Pages[0])
}
if docResult.Pages[0].Exports["docx"] != "docx-url" || docResult.Pages[0].Markdown["isStart"] != true {
t.Fatalf("document structured fields not preserved: %#v", docResult.Pages[0])
}
}
func TestSubmitOCRAcceptsOfficialModelNameString(t *testing.T) {
var got string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Model string `json:"model"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("Decode request body error = %v", err)
}
got = body.Model
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"data": map[string]string{"jobId": "job-1"},
})
}))
defer server.Close()
client, err := NewClient(
WithToken("token"),
WithBaseURL(server.URL),
)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}
job, err := client.SubmitOCR(context.Background(), &OCRRequest{
Model: "PP-OCRv5",
FileURL: "https://example.test/input.pdf",
})
if err != nil {
t.Fatalf("SubmitOCR() error = %v", err)
}
if job.Model != "PP-OCRv5" {
t.Fatalf("Job model = %q, want PP-OCRv5", job.Model)
}
if got != "PP-OCRv5" {
t.Fatalf("Request model = %q, want PP-OCRv5", got)
}
}
func TestSubmitOCRAcceptsPPOCRv5LatinModelNameString(t *testing.T) {
var got string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Model string `json:"model"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("Decode request body error = %v", err)
}
got = body.Model
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"code": 0,
"data": map[string]string{"jobId": "job-latin"},
})
}))
defer server.Close()
client, err := NewClient(
WithToken("token"),
WithBaseURL(server.URL),
)
if err != nil {
t.Fatalf("NewClient() error = %v", err)
}
job, err := client.SubmitOCR(context.Background(), &OCRRequest{
Model: PPOCRv5Latin,
FileURL: "https://example.test/latin.pdf",
})
if err != nil {
t.Fatalf("SubmitOCR() error = %v", err)
}
if job.Model != PPOCRv5Latin {
t.Fatalf("Job model = %q, want %s", job.Model, PPOCRv5Latin)
}
if got != PPOCRv5Latin {
t.Fatalf("Request model = %q, want %s", got, PPOCRv5Latin)
}
}
+30
View File
@@ -0,0 +1,30 @@
// 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.
// Package paddleocr provides a Go client for the PaddleOCR official API.
//
// Create a client with NewClient and authenticate with WithToken or the
// PADDLEOCR_ACCESS_TOKEN environment variable. Use OCR for supported OCR models
// and ParseDocument for document parsing models.
// SubmitOCR and SubmitDocumentParsing return an Operation for non-blocking
// status checks with Poll or typed waits with WaitOCR and WaitDocumentParsing.
// SaveResource downloads one result resource URL. SaveOCRResultResources and
// SaveDocumentParsingResultResources save resources from typed result objects
// into an existing directory.
//
// Request timeout and polling timeout are configured separately with
// WithRequestTimeout and WithPollTimeout. Errors are exposed as typed values,
// such as AuthError, InvalidRequestError, APIError, ResponseFormatError, and
// ResultParseError, and are suitable for errors.As.
package paddleocr
+103
View File
@@ -0,0 +1,103 @@
// 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.
package paddleocr
import "fmt"
type PaddleOCRAPIError struct {
Message string
Cause error
}
func (e *PaddleOCRAPIError) Error() string {
if e.Message == "" && e.Cause != nil {
return e.Cause.Error()
}
return e.Message
}
func (e *PaddleOCRAPIError) Unwrap() error {
return e.Cause
}
type AuthError struct {
PaddleOCRAPIError
}
type InvalidRequestError struct {
PaddleOCRAPIError
}
type APIError struct {
StatusCode int
PaddleOCRAPIError
}
func (e *APIError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}
type RateLimitError struct {
APIError
}
type ServiceUnavailableError struct {
APIError
}
type JobFailedError struct {
JobID string
ErrorMsg string
PaddleOCRAPIError
}
func (e *JobFailedError) Error() string {
return fmt.Sprintf("Job %s failed: %s", e.JobID, e.ErrorMsg)
}
type RequestTimeoutError struct {
PaddleOCRAPIError
}
type PollTimeoutError struct {
JobID string
Elapsed float64
PaddleOCRAPIError
}
func (e *PollTimeoutError) Error() string {
return fmt.Sprintf("Timed out after %.1fs waiting for job %s", e.Elapsed, e.JobID)
}
type NetworkError struct {
PaddleOCRAPIError
}
type FileNotFoundError struct {
Path string
PaddleOCRAPIError
}
func (e *FileNotFoundError) Error() string {
return fmt.Sprintf("File not found: %s", e.Path)
}
type ResponseFormatError struct {
PaddleOCRAPIError
}
type ResultParseError struct {
PaddleOCRAPIError
}
@@ -0,0 +1,61 @@
// 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.
package main
import (
"context"
"fmt"
"log"
paddleocr "github.com/PaddlePaddle/PaddleOCR/api_sdk/go"
)
func main() {
client, err := paddleocr.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Convenience method (blocks until done)
result, err := client.ParseDocument(ctx, &paddleocr.DocParsingRequest{
Model: paddleocr.PPStructureV3,
FilePath: "./sample.pdf",
Options: &paddleocr.PPStructureV3Options{UseChartRecognition: paddleocr.Bool(true)},
})
if err != nil {
log.Fatal(err)
}
for i, page := range result.Pages {
fmt.Printf("Page %d:\n%s\n", i+1, page.MarkdownText)
}
// Manual control with typed job metadata and typed wait methods.
ocrJob, _ := client.SubmitOCR(ctx, &paddleocr.OCRRequest{FileURL: "https://example.com/f1.pdf"})
docJob, _ := client.SubmitDocumentParsing(ctx, &paddleocr.DocParsingRequest{
Model: paddleocr.PPStructureV3, FilePath: "./sample.pdf",
})
ocrResult, err := client.WaitOCRResult(ctx, ocrJob.JobID)
if err != nil {
log.Printf("OCR job error: %v", err)
}
docResult, err := client.WaitDocumentParsingResult(ctx, docJob.JobID)
if err != nil {
log.Printf("document parsing job error: %v", err)
}
fmt.Printf("OCR done: %v\n", ocrResult)
fmt.Printf("Document parsing done: %v\n", docResult)
}
+43
View File
@@ -0,0 +1,43 @@
// 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.
package main
import (
"context"
"fmt"
"log"
paddleocr "github.com/PaddlePaddle/PaddleOCR/api_sdk/go"
)
func main() {
client, err := paddleocr.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
result, err := client.OCR(ctx, &paddleocr.OCRRequest{
FileURL: "https://example.com/invoice.pdf",
})
if err != nil {
log.Fatal(err)
}
for i, page := range result.Pages {
fmt.Printf("Page %d: %v\n", i+1, page.PrunedResult)
fmt.Printf(" Image URL: %s\n", page.OCRImageURL)
}
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/PaddlePaddle/PaddleOCR/api_sdk/go
go 1.21
+159
View File
@@ -0,0 +1,159 @@
// 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.
package paddleocr
const (
PPOCRv5 = "PP-OCRv5"
PPOCRv5Latin = "PP-OCRv5-latin"
PPOCRv6 = "PP-OCRv6"
PPStructureV3 = "PP-StructureV3"
PaddleOCRVL = "PaddleOCR-VL"
PaddleOCRVL15 = "PaddleOCR-VL-1.5"
PaddleOCRVL16 = "PaddleOCR-VL-1.6"
)
// IsOCRModel reports whether model is supported by OCR APIs.
func IsOCRModel(model string) bool {
return model == PPOCRv5 || model == PPOCRv5Latin || model == PPOCRv6
}
// IsDocumentParsingModel reports whether model is supported by document parsing APIs.
func IsDocumentParsingModel(model string) bool {
switch model {
case PPStructureV3, PaddleOCRVL, PaddleOCRVL15, PaddleOCRVL16:
return true
default:
return false
}
}
// IsVLModel reports whether model is a PaddleOCR-VL family model.
func IsVLModel(model string) bool {
switch model {
case PaddleOCRVL, PaddleOCRVL15, PaddleOCRVL16:
return true
default:
return false
}
}
type OCROptions struct {
UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"`
UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"`
UseTextlineOrientation *bool `json:"useTextlineOrientation,omitempty"`
TextDetLimitSideLen *int `json:"textDetLimitSideLen,omitempty"`
TextDetLimitType *string `json:"textDetLimitType,omitempty"`
TextDetThresh *float64 `json:"textDetThresh,omitempty"`
TextDetBoxThresh *float64 `json:"textDetBoxThresh,omitempty"`
TextDetUnclipRatio *float64 `json:"textDetUnclipRatio,omitempty"`
TextRecScoreThresh *float64 `json:"textRecScoreThresh,omitempty"`
Visualize *bool `json:"visualize,omitempty"`
ExtraOptions map[string]interface{} `json:"-"`
}
type PPStructureV3Options struct {
UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"`
UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"`
UseTextlineOrientation *bool `json:"useTextlineOrientation,omitempty"`
UseSealRecognition *bool `json:"useSealRecognition,omitempty"`
UseTableRecognition *bool `json:"useTableRecognition,omitempty"`
UseFormulaRecognition *bool `json:"useFormulaRecognition,omitempty"`
UseChartRecognition *bool `json:"useChartRecognition,omitempty"`
UseRegionDetection *bool `json:"useRegionDetection,omitempty"`
LayoutThreshold interface{} `json:"layoutThreshold,omitempty"`
LayoutNms *bool `json:"layoutNms,omitempty"`
LayoutUnclipRatio interface{} `json:"layoutUnclipRatio,omitempty"`
LayoutMergeBboxesMode interface{} `json:"layoutMergeBboxesMode,omitempty"`
FormatBlockContent *bool `json:"formatBlockContent,omitempty"`
TextDetLimitSideLen *int `json:"textDetLimitSideLen,omitempty"`
TextDetLimitType *string `json:"textDetLimitType,omitempty"`
TextDetThresh *float64 `json:"textDetThresh,omitempty"`
TextDetBoxThresh *float64 `json:"textDetBoxThresh,omitempty"`
TextDetUnclipRatio *float64 `json:"textDetUnclipRatio,omitempty"`
TextRecScoreThresh *float64 `json:"textRecScoreThresh,omitempty"`
UseWiredTableCellsTransToHtml *bool `json:"useWiredTableCellsTransToHtml,omitempty"`
UseWirelessTableCellsTransToHtml *bool `json:"useWirelessTableCellsTransToHtml,omitempty"`
UseTableOrientationClassify *bool `json:"useTableOrientationClassify,omitempty"`
UseOcrResultsWithTableCells *bool `json:"useOcrResultsWithTableCells,omitempty"`
UseE2eWiredTableRecModel *bool `json:"useE2eWiredTableRecModel,omitempty"`
UseE2eWirelessTableRecModel *bool `json:"useE2eWirelessTableRecModel,omitempty"`
MarkdownIgnoreLabels []string `json:"markdownIgnoreLabels,omitempty"`
PrettifyMarkdown *bool `json:"prettifyMarkdown,omitempty"`
ShowFormulaNumber *bool `json:"showFormulaNumber,omitempty"`
ReturnMarkdownImages *bool `json:"returnMarkdownImages,omitempty"`
OutputFormats []string `json:"outputFormats,omitempty"`
Visualize *bool `json:"visualize,omitempty"`
ExtraOptions map[string]interface{} `json:"-"`
}
type PaddleOCRVLOptions struct {
UseDocOrientationClassify *bool `json:"useDocOrientationClassify,omitempty"`
UseDocUnwarping *bool `json:"useDocUnwarping,omitempty"`
UseLayoutDetection *bool `json:"useLayoutDetection,omitempty"`
UseChartRecognition *bool `json:"useChartRecognition,omitempty"`
UseSealRecognition *bool `json:"useSealRecognition,omitempty"`
UseOcrForImageBlock *bool `json:"useOcrForImageBlock,omitempty"`
LayoutThreshold interface{} `json:"layoutThreshold,omitempty"`
LayoutNms *bool `json:"layoutNms,omitempty"`
LayoutUnclipRatio interface{} `json:"layoutUnclipRatio,omitempty"`
LayoutMergeBboxesMode interface{} `json:"layoutMergeBboxesMode,omitempty"`
LayoutShapeMode *string `json:"layoutShapeMode,omitempty"`
PromptLabel *string `json:"promptLabel,omitempty"`
FormatBlockContent *bool `json:"formatBlockContent,omitempty"`
RepetitionPenalty *float64 `json:"repetitionPenalty,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"topP,omitempty"`
MinPixels *int `json:"minPixels,omitempty"`
MaxPixels *int `json:"maxPixels,omitempty"`
MaxNewTokens *int `json:"maxNewTokens,omitempty"`
VlmExtraArgs map[string]interface{} `json:"vlmExtraArgs,omitempty"`
MergeLayoutBlocks *bool `json:"mergeLayoutBlocks,omitempty"`
MarkdownIgnoreLabels []string `json:"markdownIgnoreLabels,omitempty"`
PrettifyMarkdown *bool `json:"prettifyMarkdown,omitempty"`
ShowFormulaNumber *bool `json:"showFormulaNumber,omitempty"`
RestructurePages *bool `json:"restructurePages,omitempty"`
MergeTables *bool `json:"mergeTables,omitempty"`
RelevelTitles *bool `json:"relevelTitles,omitempty"`
ReturnMarkdownImages *bool `json:"returnMarkdownImages,omitempty"`
OutputFormats []string `json:"outputFormats,omitempty"`
Visualize *bool `json:"visualize,omitempty"`
ExtraOptions map[string]interface{} `json:"-"`
}
// DocParsingOptionsProvider marks document parsing option structs.
type DocParsingOptionsProvider interface {
isDocParsingOptions()
}
func (*PPStructureV3Options) isDocParsingOptions() {}
func (*PaddleOCRVLOptions) isDocParsingOptions() {}
type OCRRequest struct {
Model string
FileURL string
FilePath string
PageRanges string
BatchID string
Options *OCROptions
}
type DocParsingRequest struct {
Model string
FileURL string
FilePath string
PageRanges string
BatchID string
Options DocParsingOptionsProvider
}
+317
View File
@@ -0,0 +1,317 @@
// 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.
package paddleocr
import (
"context"
"encoding/json"
)
func (c *Client) OCR(ctx context.Context, req *OCRRequest) (*OCRResult, error) {
job, err := c.SubmitOCR(ctx, req)
if err != nil {
return nil, err
}
return c.WaitOCRResult(ctx, job.JobID)
}
// ParseDocument performs document parsing. Blocks until result is ready.
func (c *Client) ParseDocument(ctx context.Context, req *DocParsingRequest) (*DocParsingResult, error) {
job, err := c.SubmitDocumentParsing(ctx, req)
if err != nil {
return nil, err
}
return c.WaitDocumentParsingResult(ctx, job.JobID)
}
// SubmitOCR submits an OCR job and returns job metadata for tracking.
func (c *Client) SubmitOCR(ctx context.Context, req *OCRRequest) (*Job, error) {
if req == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR request is nil"}}
}
model := req.Model
if model == "" {
model = PPOCRv6
}
if !IsOCRModel(model) {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not an OCR model: " + model}}
}
jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID)
if err != nil {
return nil, err
}
return &Job{JobID: jobID, Model: model, Task: "ocr", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil
}
// SubmitDocumentParsing submits a document parsing job and returns job metadata for tracking.
func (c *Client) SubmitDocumentParsing(ctx context.Context, req *DocParsingRequest) (*Job, error) {
if req == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing request is nil"}}
}
model := req.Model
if model == "" {
model = PaddleOCRVL16
}
if !IsDocumentParsingModel(model) {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not a document parsing model: " + model}}
}
jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID)
if err != nil {
return nil, err
}
return &Job{JobID: jobID, Model: model, Task: "document_parsing", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil
}
func (c *Client) WaitOCRResult(ctx context.Context, jobID string) (*OCRResult, error) {
jsonlData, err := c.pollUntilDone(ctx, jobID)
if err != nil {
return nil, err
}
return parseOCRResult(jobID, jsonlData)
}
func (c *Client) WaitDocumentParsingResult(ctx context.Context, jobID string) (*DocParsingResult, error) {
jsonlData, err := c.pollUntilDone(ctx, jobID)
if err != nil {
return nil, err
}
return parseDocParsingResult(jobID, jsonlData)
}
func (c *Client) GetStatus(ctx context.Context, jobID string) (*JobStatus, error) {
status, err := c.getJobStatus(ctx, jobID)
if err != nil {
return nil, err
}
return normalizeStatus(jobID, status)
}
func (c *Client) GetBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) {
if batchID == "" {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "batchID is required"}}
}
return c.getBatchStatus(ctx, batchID)
}
func (c *Client) submit(ctx context.Context, model, fileURL, filePath string, options interface{}, pageRanges, batchID string) (string, error) {
if fileURL == "" && filePath == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "Either FileURL or FilePath is required."}}
}
if fileURL != "" && filePath != "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "FileURL and FilePath are mutually exclusive."}}
}
payload := defaultPayload(model, options)
if fileURL != "" {
return c.submitURL(ctx, model, fileURL, payload, pageRanges, batchID)
}
return c.submitFile(ctx, model, filePath, payload, pageRanges, batchID)
}
func defaultPayload(model string, options interface{}) interface{} {
switch typed := options.(type) {
case *OCROptions:
if typed != nil {
return payloadWithExtraOptions(typed)
}
case *PPStructureV3Options:
if typed != nil {
return payloadWithExtraOptions(typed)
}
case *PaddleOCRVLOptions:
if typed != nil {
return payloadWithExtraOptions(typed)
}
default:
if options != nil {
return payloadWithExtraOptions(options)
}
}
if IsOCRModel(model) {
return payloadWithExtraOptions(&OCROptions{})
}
if IsVLModel(model) {
return payloadWithExtraOptions(&PaddleOCRVLOptions{})
}
return payloadWithExtraOptions(&PPStructureV3Options{})
}
func payloadWithExtraOptions(options interface{}) interface{} {
payloadBytes, _ := json.Marshal(options)
payload := map[string]interface{}{}
_ = json.Unmarshal(payloadBytes, &payload)
var extraOptions map[string]interface{}
switch typed := options.(type) {
case *OCROptions:
extraOptions = typed.ExtraOptions
case *PPStructureV3Options:
extraOptions = typed.ExtraOptions
case *PaddleOCRVLOptions:
extraOptions = typed.ExtraOptions
}
for key, value := range extraOptions {
payload[key] = value
}
return payload
}
func parseOCRResult(jobID string, jsonlData []map[string]interface{}) (*OCRResult, error) {
result := &OCRResult{JobID: jobID, DataInfo: map[string]interface{}{}}
for _, lineObj := range jsonlData {
resultData, ok := lineObj["result"].(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing result"}}
}
if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok {
for key, value := range dataInfo {
result.DataInfo[key] = value
}
}
ocrResults, ok := resultData["ocrResults"].([]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing ocrResults"}}
}
for _, item := range ocrResults {
itemMap, ok := item.(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is malformed"}}
}
if _, ok := itemMap["prunedResult"]; !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is missing prunedResult"}}
}
page := OCRPage{
PrunedResult: itemMap["prunedResult"],
OCRImageURL: getString(itemMap, "ocrImage"),
DocPreprocessingImageURL: getString(itemMap, "docPreprocessingImage"),
InputImageURL: getString(itemMap, "inputImage"),
Raw: itemMap,
}
result.Pages = append(result.Pages, page)
}
}
return result, nil
}
func parseDocParsingResult(jobID string, jsonlData []map[string]interface{}) (*DocParsingResult, error) {
result := &DocParsingResult{JobID: jobID, DataInfo: map[string]interface{}{}}
for _, lineObj := range jsonlData {
resultData, ok := lineObj["result"].(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing result"}}
}
if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok {
for key, value := range dataInfo {
result.DataInfo[key] = value
}
}
lpResults, ok := resultData["layoutParsingResults"].([]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing layoutParsingResults"}}
}
for _, item := range lpResults {
itemMap, ok := item.(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is malformed"}}
}
markdown, ok := itemMap["markdown"].(map[string]interface{})
if !ok || getString(markdown, "text") == "" {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is missing markdown.text"}}
}
page := DocParsingPage{
MarkdownText: getString(markdown, "text"),
MarkdownImages: getStringMap(markdown, "images"),
OutputImages: getStringMap(itemMap, "outputImages"),
PrunedResult: itemMap["prunedResult"],
InputImageURL: getString(itemMap, "inputImage"),
Exports: getMap(itemMap, "exports"),
Markdown: markdown,
Raw: itemMap,
}
result.Pages = append(result.Pages, page)
}
}
return result, nil
}
func getString(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
v, _ := m[key].(string)
return v
}
func getMap(m map[string]interface{}, key string) map[string]interface{} {
value, ok := m[key].(map[string]interface{})
if !ok {
return map[string]interface{}{}
}
return value
}
func getStringMap(m map[string]interface{}, key string) map[string]string {
if m == nil {
return nil
}
raw, ok := m[key]
if !ok {
return nil
}
switch v := raw.(type) {
case map[string]interface{}:
result := make(map[string]string, len(v))
for k, val := range v {
if s, ok := val.(string); ok {
result[k] = s
}
}
return result
default:
b, _ := json.Marshal(raw)
var result map[string]string
json.Unmarshal(b, &result)
return result
}
}
func normalizeStatus(jobID string, status *jobStatusResponse) (*JobStatus, error) {
if status == nil || status.State == "" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is missing state"}}
}
if status.State != "pending" && status.State != "running" && status.State != "done" && status.State != "failed" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "unknown job state: " + status.State}}
}
result := &JobStatus{
JobID: jobID,
State: status.State,
ResultURL: status.ResultURL,
ErrorMsg: status.ErrorMsg,
}
if len(status.ExtractProgress) != 0 && string(status.ExtractProgress) != "null" {
var progress extractProgress
if err := json.Unmarshal(status.ExtractProgress, &progress); err != nil {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status progress is malformed", Cause: err}}
}
result.Progress = &Progress{
TotalPages: progress.TotalPages,
ExtractedPages: progress.ExtractedPages,
StartTime: progress.StartTime,
EndTime: progress.EndTime,
}
}
return result, nil
}
+69
View File
@@ -0,0 +1,69 @@
// 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.
package paddleocr
import (
"context"
"encoding/json"
)
// Operation represents an in-progress job. Use Wait() to block until done,
// or Poll() to check status without blocking.
type Operation struct {
client *Client
JobID string
model string
}
// Wait blocks until the job completes and returns the parsed result.
func (op *Operation) Wait(ctx context.Context) (interface{}, error) {
jsonlData, err := op.client.pollUntilDone(ctx, op.JobID)
if err != nil {
return nil, err
}
if IsOCRModel(op.model) {
return parseOCRResult(op.JobID, jsonlData)
}
return parseDocParsingResult(op.JobID, jsonlData)
}
// Poll checks the current job status without waiting.
// Returns the status, whether the job is done, and any error.
func (op *Operation) Poll(ctx context.Context) (*JobStatus, bool, error) {
status, err := op.client.getJobStatus(ctx, op.JobID)
if err != nil {
return nil, false, err
}
js := &JobStatus{
JobID: op.JobID,
State: status.State,
ErrorMsg: status.ErrorMsg,
}
if status.ExtractProgress != nil {
var ep extractProgress
if err := json.Unmarshal(status.ExtractProgress, &ep); err == nil {
js.Progress = &Progress{
TotalPages: ep.TotalPages,
ExtractedPages: ep.ExtractedPages,
StartTime: ep.StartTime,
EndTime: ep.EndTime,
}
}
}
return js, status.State == "done", nil
}
+73
View File
@@ -0,0 +1,73 @@
// 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.
package paddleocr
import (
"net/http"
"time"
)
const DefaultBaseURL = "https://paddleocr.aistudio-app.com"
const apiPath = "/api/v2/ocr/jobs"
type ClientOption func(*Client)
func WithToken(token string) ClientOption {
return func(c *Client) {
c.token = token
}
}
func WithBaseURL(url string) ClientOption {
return func(c *Client) {
c.baseURL = url
}
}
func WithTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.requestTimeout = d
c.pollTimeout = d
}
}
func WithRequestTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.requestTimeout = d
}
}
func WithPollTimeout(d time.Duration) ClientOption {
return func(c *Client) {
c.pollTimeout = d
}
}
func WithClientPlatform(clientPlatform string) ClientOption {
return func(c *Client) {
c.clientPlatform = clientPlatform
}
}
func WithHTTPClient(hc *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = hc
}
}
func Bool(v bool) *bool {
return &v
}
+90
View File
@@ -0,0 +1,90 @@
// 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.
package paddleocr
import (
"context"
"fmt"
"time"
)
const (
initialInterval = 3 * time.Second
multiplier = 1.5
maxInterval = 15 * time.Second
)
func (c *Client) pollUntilDone(ctx context.Context, jobID string) ([]map[string]interface{}, error) {
interval := initialInterval
deadline := time.Now().Add(c.pollTimeout)
pollCtx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
for time.Now().Before(deadline) {
remaining := time.Until(deadline)
if remaining <= 0 {
break
}
if interval > remaining {
interval = remaining
}
timer := time.NewTimer(interval)
select {
case <-pollCtx.Done():
timer.Stop()
if ctx.Err() != nil {
return nil, ctx.Err()
}
return nil, &PollTimeoutError{
JobID: jobID,
Elapsed: c.pollTimeout.Seconds(),
PaddleOCRAPIError: PaddleOCRAPIError{Message: fmt.Sprintf("Timed out after %.1fs", c.pollTimeout.Seconds())},
}
case <-timer.C:
}
status, err := c.getJobStatus(pollCtx, jobID)
if err != nil {
return nil, err
}
switch status.State {
case "done":
jsonURL := status.ResultURL["jsonUrl"]
if jsonURL == "" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "done job response is missing resultUrl.jsonUrl"}}
}
return c.fetchJSONL(pollCtx, jsonURL)
case "failed":
return nil, &JobFailedError{
JobID: jobID,
ErrorMsg: status.ErrorMsg,
PaddleOCRAPIError: PaddleOCRAPIError{Message: status.ErrorMsg},
}
}
next := time.Duration(float64(interval) * multiplier)
if next > maxInterval {
next = maxInterval
}
interval = next
}
return nil, &PollTimeoutError{
JobID: jobID,
Elapsed: c.pollTimeout.Seconds(),
PaddleOCRAPIError: PaddleOCRAPIError{Message: fmt.Sprintf("Timed out after %.1fs", c.pollTimeout.Seconds())},
}
}
+267
View File
@@ -0,0 +1,267 @@
// 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.
package paddleocr
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
)
type SaveResourceOption func(*saveResourceOptions)
type saveResourceOptions struct {
overwrite bool
}
func WithOverwrite(overwrite bool) SaveResourceOption {
return func(o *saveResourceOptions) {
o.overwrite = overwrite
}
}
// SaveResource downloads one result resource URL.
func (c *Client) SaveResource(ctx context.Context, resourceURL, dest string, opts ...SaveResourceOption) (string, error) {
if resourceURL == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "resource URL is required"}}
}
if dest == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination path is required"}}
}
options := saveResourceOptions{}
for _, opt := range opts {
opt(&options)
}
savedPath, err := resolveSavePath(resourceURL, dest)
if err != nil {
return "", err
}
parent := filepath.Dir(savedPath)
if st, err := os.Stat(parent); err != nil {
if errors.Is(err, os.ErrNotExist) {
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent not found: " + parent, Cause: err}}
}
return "", err
} else if !st.IsDir() {
return "", &FileNotFoundError{Path: parent, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination parent is not a directory: " + parent}}
}
if !options.overwrite {
if _, err := os.Stat(savedPath); err == nil {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath}}
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", err
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
if err != nil {
return "", &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return "", classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResourceResponse(resp); err != nil {
return "", err
}
tmp, err := os.CreateTemp(parent, "."+filepath.Base(savedPath)+".tmp-*")
if err != nil {
return "", err
}
tmpPath := tmp.Name()
cleanupTemp := true
defer func() {
if cleanupTemp {
_ = os.Remove(tmpPath)
}
}()
if _, err := io.Copy(tmp, resp.Body); err != nil {
_ = tmp.Close()
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
if options.overwrite {
if err := os.Rename(tmpPath, savedPath); err != nil {
return "", err
}
} else {
if err := os.Link(tmpPath, savedPath); err != nil {
if errors.Is(err, os.ErrExist) {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "destination already exists: " + savedPath, Cause: err}}
}
return "", err
}
if err := os.Remove(tmpPath); err != nil {
return "", err
}
}
cleanupTemp = false
return savedPath, nil
}
func (c *Client) SaveOCRResultResources(ctx context.Context, result *OCRResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
if result == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR result is required"}}
}
if err := requireExistingDirectory(destDir); err != nil {
return nil, err
}
saved := make([]string, 0, len(result.Pages))
for i, page := range result.Pages {
if page.OCRImageURL == "" {
continue
}
name := fmt.Sprintf("ocr-page-%d%s", i+1, safeResourceExtension(page.OCRImageURL))
if err := validateResultResourceFilename(name); err != nil {
return nil, err
}
path, err := c.SaveResource(ctx, page.OCRImageURL, filepath.Join(destDir, name), opts...)
if err != nil {
return nil, err
}
saved = append(saved, path)
}
return saved, nil
}
func (c *Client) SaveDocumentParsingResultResources(ctx context.Context, result *DocParsingResult, destDir string, opts ...SaveResourceOption) ([]string, error) {
if result == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing result is required"}}
}
if err := requireExistingDirectory(destDir); err != nil {
return nil, err
}
var saved []string
for _, page := range result.Pages {
paths, err := c.saveNamedResourceMap(ctx, page.MarkdownImages, destDir, opts...)
if err != nil {
return nil, err
}
saved = append(saved, paths...)
paths, err = c.saveNamedResourceMap(ctx, page.OutputImages, destDir, opts...)
if err != nil {
return nil, err
}
saved = append(saved, paths...)
}
return saved, nil
}
func (c *Client) saveNamedResourceMap(ctx context.Context, resources map[string]string, destDir string, opts ...SaveResourceOption) ([]string, error) {
keys := make([]string, 0, len(resources))
for key := range resources {
keys = append(keys, key)
}
sort.Strings(keys)
saved := make([]string, 0, len(keys))
for _, key := range keys {
resourceURL := resources[key]
if resourceURL == "" {
continue
}
name, err := resultResourceFilename(key)
if err != nil {
return nil, err
}
path, err := c.SaveResource(ctx, resourceURL, filepath.Join(destDir, name), opts...)
if err != nil {
return nil, err
}
saved = append(saved, path)
}
return saved, nil
}
func resolveSavePath(resourceURL, dest string) (string, error) {
if st, err := os.Stat(dest); err == nil && st.IsDir() {
name := "resource"
u, parseErr := url.Parse(resourceURL)
if parseErr == nil {
if base := path.Base(u.Path); base != "." && base != "/" && base != "" {
name = base
}
}
return filepath.Join(dest, name), nil
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
return "", err
}
return dest, nil
}
func raiseForResourceResponse(resp *http.Response) error {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: string(body)}}
}
func requireExistingDirectory(destDir string) error {
if destDir == "" {
return &InvalidRequestError{PaddleOCRAPIError{Message: "destination directory is required"}}
}
st, err := os.Stat(destDir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination directory not found: " + destDir, Cause: err}}
}
return err
}
if !st.IsDir() {
return &FileNotFoundError{Path: destDir, PaddleOCRAPIError: PaddleOCRAPIError{Message: "destination is not a directory: " + destDir}}
}
return nil
}
func resultResourceFilename(key string) (string, error) {
if err := validateResultResourceFilename(key); err != nil {
return "", err
}
return key, nil
}
func validateResultResourceFilename(name string) error {
if name == "" || name == "." || name == ".." || filepath.IsAbs(name) || path.IsAbs(name) || strings.ContainsAny(name, `/\`) {
return &InvalidRequestError{PaddleOCRAPIError{Message: "unsafe resource filename: " + name}}
}
return nil
}
func safeResourceExtension(resourceURL string) string {
u, err := url.Parse(resourceURL)
if err != nil {
return ""
}
ext := path.Ext(path.Base(u.Path))
if len(ext) <= 1 {
return ""
}
if err := validateResultResourceFilename("resource" + ext); err != nil {
return ""
}
return ext
}
+74
View File
@@ -0,0 +1,74 @@
// 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.
package paddleocr
type OCRPage struct {
PrunedResult interface{} `json:"prunedResult"`
OCRImageURL string `json:"ocrImageUrl,omitempty"`
DocPreprocessingImageURL string `json:"docPreprocessingImageUrl,omitempty"`
InputImageURL string `json:"inputImageUrl,omitempty"`
Raw map[string]interface{} `json:"raw,omitempty"`
}
type DocParsingPage struct {
MarkdownText string `json:"markdownText"`
MarkdownImages map[string]string `json:"markdownImages"`
OutputImages map[string]string `json:"outputImages"`
PrunedResult interface{} `json:"prunedResult,omitempty"`
InputImageURL string `json:"inputImageUrl,omitempty"`
Exports map[string]interface{} `json:"exports,omitempty"`
Markdown map[string]interface{} `json:"markdown,omitempty"`
Raw map[string]interface{} `json:"raw,omitempty"`
}
type OCRResult struct {
JobID string `json:"jobId"`
Pages []OCRPage `json:"pages"`
DataInfo map[string]interface{} `json:"dataInfo,omitempty"`
}
type DocParsingResult struct {
JobID string `json:"jobId"`
Pages []DocParsingPage `json:"pages"`
DataInfo map[string]interface{} `json:"dataInfo,omitempty"`
}
type Progress struct {
TotalPages int `json:"totalPages"`
ExtractedPages int `json:"extractedPages"`
StartTime string `json:"startTime,omitempty"`
EndTime string `json:"endTime,omitempty"`
}
type Job struct {
JobID string `json:"jobId"`
Model string `json:"model"`
Task string `json:"task"`
PageRanges string `json:"pageRanges,omitempty"`
BatchID string `json:"batchId,omitempty"`
}
type JobStatus struct {
JobID string `json:"jobId"`
State string `json:"state"`
Progress *Progress `json:"progress,omitempty"`
ResultURL map[string]string `json:"resultUrl,omitempty"`
ErrorMsg string `json:"errorMsg,omitempty"`
}
type BatchStatus struct {
BatchID string `json:"batchId"`
Jobs []*JobStatus `json:"jobs"`
}
+329
View File
@@ -0,0 +1,329 @@
// 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.
package paddleocr
import (
"bytes"
"context"
"encoding/json"
"io"
"mime/multipart"
"net"
"net/http"
"os"
"path/filepath"
"strings"
)
type apiResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data json.RawMessage `json:"data"`
}
type submitResponse struct {
JobID string `json:"jobId"`
}
type jobStatusResponse struct {
JobID string `json:"jobId"`
State string `json:"state"`
ExtractProgress json.RawMessage `json:"extractProgress"`
ResultURL map[string]string `json:"resultUrl"`
ErrorMsg string `json:"errorMsg"`
}
type extractProgress struct {
TotalPages int `json:"totalPages"`
ExtractedPages int `json:"extractedPages"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
}
func (c *Client) submitURL(ctx context.Context, model, fileURL string, payload interface{}, pageRanges, batchID string) (string, error) {
body := map[string]interface{}{
"fileUrl": fileURL,
"model": model,
"optionalPayload": payload,
}
if pageRanges != "" {
body["pageRanges"] = pageRanges
}
if batchID != "" {
body["batchId"] = batchID
}
jsonBody, err := json.Marshal(body)
if err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to encode submit request", Cause: err}}
}
req, err := http.NewRequestWithContext(ctx, "POST", c.jobsURL, bytes.NewReader(jsonBody))
if err != nil {
return "", &NetworkError{PaddleOCRAPIError{Message: err.Error()}}
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
c.setClientPlatformHeader(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return "", classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResponse(resp); err != nil {
return "", err
}
apiResp, err := decodeAPIResponse(resp)
if err != nil {
return "", err
}
var sr submitResponse
if err := json.Unmarshal(apiResp.Data, &sr); err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is malformed", Cause: err}}
}
if sr.JobID == "" {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is missing jobId"}}
}
return sr.JobID, nil
}
func (c *Client) submitFile(ctx context.Context, model, filePath string, payload interface{}, pageRanges, batchID string) (string, error) {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return "", &FileNotFoundError{Path: filePath, PaddleOCRAPIError: PaddleOCRAPIError{Message: "File not found: " + filePath}}
}
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
_ = w.WriteField("model", model)
payloadJSON, err := json.Marshal(payload)
if err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to encode optionalPayload", Cause: err}}
}
if err := w.WriteField("optionalPayload", string(payloadJSON)); err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to prepare multipart body", Cause: err}}
}
if pageRanges != "" {
_ = w.WriteField("pageRanges", pageRanges)
}
if batchID != "" {
_ = w.WriteField("batchId", batchID)
}
file, err := os.Open(filePath)
if err != nil {
return "", &FileNotFoundError{Path: filePath, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Failed to open file: " + filePath, Cause: err}}
}
defer file.Close()
fw, err := w.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to prepare multipart body", Cause: err}}
}
if _, err := io.Copy(fw, file); err != nil {
return "", &NetworkError{PaddleOCRAPIError{Message: "failed to read file content", Cause: err}}
}
if err := w.Close(); err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "failed to finalize multipart body", Cause: err}}
}
req, err := http.NewRequestWithContext(ctx, "POST", c.jobsURL, &buf)
if err != nil {
return "", &NetworkError{PaddleOCRAPIError{Message: err.Error()}}
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", w.FormDataContentType())
c.setClientPlatformHeader(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return "", classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResponse(resp); err != nil {
return "", err
}
apiResp, err := decodeAPIResponse(resp)
if err != nil {
return "", err
}
var sr submitResponse
if err := json.Unmarshal(apiResp.Data, &sr); err != nil {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is malformed", Cause: err}}
}
if sr.JobID == "" {
return "", &ResponseFormatError{PaddleOCRAPIError{Message: "submit response is missing jobId"}}
}
return sr.JobID, nil
}
func (c *Client) getJobStatus(ctx context.Context, jobID string) (*jobStatusResponse, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.jobsURL+"/"+jobID, nil)
if err != nil {
return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error()}}
}
req.Header.Set("Authorization", "Bearer "+c.token)
c.setClientPlatformHeader(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResponse(resp); err != nil {
return nil, err
}
apiResp, err := decodeAPIResponse(resp)
if err != nil {
return nil, err
}
var status jobStatusResponse
if err := json.Unmarshal(apiResp.Data, &status); err != nil {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is malformed", Cause: err}}
}
if status.State == "" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is missing state"}}
}
return &status, nil
}
func (c *Client) getBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.jobsURL+"/batch/"+batchID, nil)
if err != nil {
return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}}
}
req.Header.Set("Authorization", "Bearer "+c.token)
c.setClientPlatformHeader(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResponse(resp); err != nil {
return nil, err
}
apiResp, err := decodeAPIResponse(resp)
if err != nil {
return nil, err
}
var payload struct {
ExtractResult []jobStatusResponse `json:"extractResult"`
}
if err := json.Unmarshal(apiResp.Data, &payload); err != nil {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "batch response is malformed", Cause: err}}
}
result := &BatchStatus{BatchID: batchID}
for _, item := range payload.ExtractResult {
jobID := item.JobID
if jobID == "" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "batch response item is missing jobId"}}
}
status, err := normalizeStatus(jobID, &item)
if err != nil {
return nil, err
}
result.Jobs = append(result.Jobs, status)
}
return result, nil
}
func (c *Client) fetchJSONL(ctx context.Context, url string) ([]map[string]interface{}, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, &NetworkError{PaddleOCRAPIError{Message: err.Error()}}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, classifyHTTPError(err)
}
defer resp.Body.Close()
if err := raiseForResponse(resp); err != nil {
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
var results []map[string]interface{}
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var obj map[string]interface{}
if err := json.Unmarshal([]byte(line), &obj); err != nil {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "failed to parse JSONL result payload", Cause: err}}
}
results = append(results, obj)
}
return results, nil
}
func raiseForResponse(resp *http.Response) error {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
msg := string(body)
switch {
case resp.StatusCode == 401 || resp.StatusCode == 403:
return &AuthError{PaddleOCRAPIError{Message: "Authentication failed: " + msg}}
case resp.StatusCode == 400:
return &InvalidRequestError{PaddleOCRAPIError{Message: "Bad request: " + msg}}
case resp.StatusCode == 429:
return &RateLimitError{APIError{StatusCode: 429, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Rate limit exceeded: " + msg}}}
case resp.StatusCode == 503 || resp.StatusCode == 504:
return &ServiceUnavailableError{APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: "Service unavailable: " + msg}}}
default:
return &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: msg}}
}
}
func decodeAPIResponse(resp *http.Response) (*apiResponse, error) {
var apiResp apiResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "expected a JSON response body", Cause: err}}
}
if apiResp.Code != 0 {
msg := apiResp.Msg
if msg == "" {
msg = "PaddleOCR official API request failed"
}
return nil, &APIError{StatusCode: resp.StatusCode, PaddleOCRAPIError: PaddleOCRAPIError{Message: msg}}
}
if len(apiResp.Data) == 0 || string(apiResp.Data) == "null" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "response body is missing data"}}
}
return &apiResp, nil
}
func classifyHTTPError(err error) error {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return &RequestTimeoutError{PaddleOCRAPIError: PaddleOCRAPIError{Message: err.Error(), Cause: err}}
}
return &NetworkError{PaddleOCRAPIError{Message: err.Error(), Cause: err}}
}
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
.vite/
coverage/
.nyc_output/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
+72
View File
@@ -0,0 +1,72 @@
# PaddleOCR TypeScript SDK
English | [简体中文](README_cn.md)
TypeScript client for the PaddleOCR official API. It submits OCR and document
parsing jobs to hosted PaddleOCR services; it does not perform local OCR
inference.
Official user docs:
- [TypeScript SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md)
- [TypeScript SDK (English)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.en.md)
## Install
```bash
npm install @paddleocr/api-sdk
```
This package follows SemVer and is published as a public scoped npm package.
For local development:
```bash
npm install
npm run build
```
## Minimal Usage
Set `PADDLEOCR_ACCESS_TOKEN` or pass `token` to the client:
```bash
export PADDLEOCR_ACCESS_TOKEN="your-access-token"
```
```ts
import { Model, PaddleOCRClient } from "@paddleocr/api-sdk";
const client = new PaddleOCRClient();
const result = await client.ocr({
model: Model.PPOCRv5,
fileUrl: "https://example.com/invoice.pdf",
});
console.log(result.jobId, result.pages.length);
```
Specify `model: Model.PPOCRv6` (or `"PP-OCRv6"`) to use the PP-OCRv6 hosted OCR model.
Use `Model.PPOCRv5Latin` (or `"PP-OCRv5-latin"`) for the PP-OCRv5 Latin-script hosted OCR model.
Document parsing defaults to PaddleOCR-VL-1.6:
```ts
const doc = await client.parseDocument({
filePath: "./report.pdf",
options: {
useChartRecognition: true,
},
});
console.log(doc.jobId, doc.pages.length);
```
## Build And Test
```bash
npm run lint
npm run build
npm test
npm audit --audit-level=moderate
```
+70
View File
@@ -0,0 +1,70 @@
# PaddleOCR TypeScript SDK
[English](README.md) | 简体中文
面向 PaddleOCR 官方 API 的 TypeScript 客户端。它会把 OCR 和文档解析任务提交到 PaddleOCR 官方托管服务;不会在本地执行 OCR 推理。
正式用户文档:
- [TypeScript SDK](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.md)
- [TypeScript SDK(英文)](../../docs/version3.x/inference_deployment/serving/paddleocr_official_api/typescript.en.md)
## 安装
```bash
npm install @paddleocr/api-sdk
```
该包遵循语义化版本,并作为公开 scoped npm 包发布。
本地开发:
```bash
npm install
npm run build
```
## 最小示例
设置 `PADDLEOCR_ACCESS_TOKEN`,或在构造客户端时传入 `token`
```bash
export PADDLEOCR_ACCESS_TOKEN="your-access-token"
```
```ts
import { Model, PaddleOCRClient } from "@paddleocr/api-sdk";
const client = new PaddleOCRClient();
const result = await client.ocr({
model: Model.PPOCRv5,
fileUrl: "https://example.com/invoice.pdf",
});
console.log(result.jobId, result.pages.length);
```
通过 `model: Model.PPOCRv6`(或 `"PP-OCRv6"`)可指定 PP-OCRv6 云端 OCR 模型。
使用 `Model.PPOCRv5Latin`(或 `"PP-OCRv5-latin"`)可指定 PP-OCRv5 拉丁语系云端 OCR 模型。
文档解析默认使用 PaddleOCR-VL-1.6
```ts
const doc = await client.parseDocument({
filePath: "./report.pdf",
options: {
useChartRecognition: true,
},
});
console.log(doc.jobId, doc.pages.length);
```
## 构建与测试
```bash
npm run lint
npm run build
npm test
npm audit --audit-level=moderate
```
@@ -0,0 +1,47 @@
// 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.
import { PaddleOCRClient, Model } from "@paddleocr/api-sdk";
async function main() {
const client = new PaddleOCRClient();
// Convenience: doc parsing with local file
const result = await client.parseDocument({
model: Model.PPStructureV3,
filePath: "./sample.pdf",
options: { useChartRecognition: true },
});
for (const page of result.pages) {
console.log(page.markdownText);
}
// Manual control: submit + concurrent wait
const job1 = await client.submitOcr({ fileUrl: "https://example.com/f1.pdf" });
const job2 = await client.submitDocumentParsing({
model: Model.PPStructureV3,
filePath: "./sample.pdf",
});
const [r1, r2] = await Promise.all([
client.waitOcrResult(job1.jobId),
client.waitDocumentParsingResult(job2.jobId),
]);
console.log("Job1 done:", r1);
console.log("Job2 done:", r2);
}
main().catch(console.error);
+31
View File
@@ -0,0 +1,31 @@
// 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.
import { PaddleOCRClient, Model } from "@paddleocr/api-sdk";
async function main() {
const client = new PaddleOCRClient();
// Convenience: OCR with URL
const ocrResult = await client.ocr({
fileUrl: "https://example.com/invoice.pdf",
});
for (const page of ocrResult.pages) {
console.log("Result:", page.prunedResult);
console.log("Image:", page.ocrImageUrl);
}
}
main().catch(console.error);
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
{
"name": "@paddleocr/api-sdk",
"version": "0.2.3",
"description": "TypeScript SDK for the PaddleOCR official API",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist",
"README.md",
"README_cn.md"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/PaddlePaddle/PaddleOCR.git",
"directory": "api_sdk/typescript"
},
"homepage": "https://github.com/PaddlePaddle/PaddleOCR/tree/main/api_sdk/typescript",
"bugs": {
"url": "https://github.com/PaddlePaddle/PaddleOCR/issues"
},
"keywords": [
"paddleocr",
"ocr",
"document-parsing",
"api-sdk",
"typescript",
"official-api"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"lint": "tsc --noEmit",
"prepublishOnly": "npm run lint && npm run build && npm test"
},
"engines": {
"node": ">=18"
},
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^25.9.1",
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^4.1.7"
}
}
+394
View File
@@ -0,0 +1,394 @@
// 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.
import { stat, writeFile } from "node:fs/promises";
import { basename, dirname, extname, join } from "node:path";
import { AuthError, FileNotFoundError, InvalidRequestError, ResultParseError } from "./errors.js";
import { HttpClient } from "./internal/http.js";
import { Poller } from "./internal/poller.js";
import type { ClientOptions, DocParsingRequest, OCRRequest, SaveResourceOptions } from "./models.js";
import { isDocumentParsingModel, isOCRModel, Model } from "./models.js";
import type { BatchStatus, DocParsingResult, Job, JobStatus, OCRResult } from "./results.js";
const DEFAULT_BASE_URL = "https://paddleocr.aistudio-app.com";
interface ResourceSavePlan {
resourceUrl: string;
filename: string;
}
export class PaddleOCRClient {
private http: HttpClient;
private poller: Poller;
constructor(options: ClientOptions = {}) {
const token = options.token || process.env.PADDLEOCR_ACCESS_TOKEN || "";
if (!token) {
throw new AuthError("Token is required. Set PADDLEOCR_ACCESS_TOKEN or pass token option.");
}
const baseUrl = options.baseUrl || process.env.PADDLEOCR_BASE_URL || DEFAULT_BASE_URL;
const requestTimeout = options.requestTimeout || options.timeout || 300000;
const pollTimeout = options.pollTimeout || options.timeout || 600000;
this.http = new HttpClient(
token,
baseUrl,
requestTimeout,
options.fetch,
options.clientPlatform,
);
this.poller = new Poller(this.http, pollTimeout);
}
async ocr(req: OCRRequest, options?: { signal?: AbortSignal }): Promise<OCRResult> {
const job = await this.submitOcr(req, options);
return this.waitOcrResult(job, options);
}
async parseDocument(req: DocParsingRequest, options?: { signal?: AbortSignal }): Promise<DocParsingResult> {
const job = await this.submitDocumentParsing(req, options);
return this.waitDocumentParsingResult(job, options);
}
async submitOcr(req: OCRRequest, options?: { signal?: AbortSignal }): Promise<Job> {
const model = req.model ?? Model.PPOCRv6;
const jobId = await this.submit(model, "ocr", req, options?.signal);
return { jobId, model, task: "ocr", pageRanges: req.pageRanges, batchId: req.batchId };
}
async submitDocumentParsing(req: DocParsingRequest, options?: { signal?: AbortSignal }): Promise<Job> {
const model = req.model ?? Model.PaddleOCRVL16;
if (!isDocumentParsingModel(model)) {
throw new InvalidRequestError(`Model ${model} is not a document parsing model.`);
}
const jobId = await this.submit(model, "document_parsing", req, options?.signal);
return { jobId, model, task: "document_parsing", pageRanges: req.pageRanges, batchId: req.batchId };
}
async waitOcrResult(job: Job | string, options?: { signal?: AbortSignal }): Promise<OCRResult> {
const resolved = this.resolveJob(job, "ocr");
const jsonlData = await this.poller.pollUntilDone(resolved.jobId, options?.signal);
return this.parseOCRResult(resolved.jobId, jsonlData);
}
async waitDocumentParsingResult(job: Job | string, options?: { signal?: AbortSignal }): Promise<DocParsingResult> {
const resolved = this.resolveJob(job, "document_parsing");
const jsonlData = await this.poller.pollUntilDone(resolved.jobId, options?.signal);
return this.parseDocParsingResult(resolved.jobId, jsonlData);
}
async getStatus(jobId: string, options?: { signal?: AbortSignal }): Promise<JobStatus> {
return this.poller.getStatus(jobId, options?.signal);
}
async getBatchStatus(batchId: string, options?: { signal?: AbortSignal }): Promise<BatchStatus> {
return this.poller.getBatchStatus(batchId, options?.signal);
}
async saveResource(
resourceUrl: string,
destination: string,
options: SaveResourceOptions = {},
): Promise<string> {
return this.saveResourceUrl(resourceUrl, destination, options);
}
async saveOcrResultResources(
result: OCRResult,
destination: string,
options: SaveResourceOptions = {},
): Promise<string[]> {
return this.saveResultResources(result, destination, options);
}
async saveDocumentParsingResultResources(
result: DocParsingResult,
destination: string,
options: SaveResourceOptions = {},
): Promise<string[]> {
return this.saveResultResources(result, destination, options);
}
private async saveResourceUrl(resourceUrl: string, destination: string, options: SaveResourceOptions): Promise<string> {
if (!resourceUrl) {
throw new InvalidRequestError("resourceUrl is required.");
}
let url: URL;
try {
url = new URL(resourceUrl);
} catch (error) {
throw new InvalidRequestError(`Invalid resource URL: ${resourceUrl}`, { cause: error });
}
const target = await this.resolveDestination(url, destination, options);
const content = await this.http.fetchResource(resourceUrl);
await writeFile(target, Buffer.from(content), { flag: options.overwrite ? "w" : "wx" });
return target;
}
private async saveResultResources(
result: OCRResult | DocParsingResult,
destination: string,
options: SaveResourceOptions,
): Promise<string[]> {
await this.requireExistingDirectory(destination);
const plans = this.collectResultResourcePlans(result);
const targets = plans.map((plan) => join(destination, plan.filename));
for (const target of targets) {
await this.requireWritableTarget(target, options);
}
this.requireUniqueTargets(targets, options);
const savedPaths: string[] = [];
for (const [index, plan] of plans.entries()) {
await this.saveResourceUrl(plan.resourceUrl, targets[index], options);
savedPaths.push(targets[index]);
}
return savedPaths;
}
private collectResultResourcePlans(result: OCRResult | DocParsingResult): ResourceSavePlan[] {
if (isDocParsingResult(result)) {
return result.pages.flatMap((page) => [
...this.collectMappedResourcePlans(page.markdownImages),
...this.collectMappedResourcePlans(page.outputImages),
]);
}
return result.pages.flatMap((page, index) => {
if (!page.ocrImageUrl) {
return [];
}
return [{
resourceUrl: page.ocrImageUrl,
filename: `ocr-page-${index + 1}${resourceExtension(page.ocrImageUrl)}`,
}];
});
}
private collectMappedResourcePlans(resources: Record<string, string>): ResourceSavePlan[] {
return Object.keys(resources)
.sort()
.map((key) => ({
resourceUrl: resources[key],
filename: safeMapKeyFilename(key),
}));
}
private async requireExistingDirectory(destination: string): Promise<void> {
let destinationStat;
try {
destinationStat = await stat(destination);
} catch {
throw new FileNotFoundError(destination);
}
if (!destinationStat.isDirectory()) {
throw new InvalidRequestError(`Destination must be an existing directory: ${destination}`);
}
}
private async requireWritableTarget(target: string, options: SaveResourceOptions): Promise<void> {
try {
await stat(target);
} catch {
return;
}
if (!options.overwrite) {
throw new InvalidRequestError(`Destination already exists: ${target}`);
}
}
private requireUniqueTargets(targets: string[], options: SaveResourceOptions): void {
if (options.overwrite) {
return;
}
const seen = new Set<string>();
for (const target of targets) {
if (seen.has(target)) {
throw new InvalidRequestError(`Destination already exists: ${target}`);
}
seen.add(target);
}
}
private async resolveDestination(url: URL, destination: string, options: SaveResourceOptions): Promise<string> {
let destinationStat;
try {
destinationStat = await stat(destination);
} catch {
destinationStat = undefined;
}
const target = destinationStat?.isDirectory() ? join(destination, safeUrlBasename(url)) : destination;
await this.requireWritableTarget(target, options);
const parent = dirname(target);
try {
const parentStat = await stat(parent);
if (!parentStat.isDirectory()) {
throw new InvalidRequestError(`Destination parent must be a directory: ${parent}`);
}
} catch (error) {
if (error instanceof InvalidRequestError) {
throw error;
}
throw new FileNotFoundError(parent, { cause: error });
}
return target;
}
private async submit(
model: string,
task: Job["task"],
req: { fileUrl?: string; filePath?: string; pageRanges?: string; batchId?: string; options?: object },
signal?: AbortSignal,
): Promise<string> {
if (!req.fileUrl && !req.filePath) {
throw new InvalidRequestError("Either fileUrl or filePath is required.");
}
if (req.fileUrl && req.filePath) {
throw new InvalidRequestError("fileUrl and filePath are mutually exclusive.");
}
this.validateModelForTask(model, task);
const payload = req.options || {};
if (req.fileUrl) {
return this.http.submitUrl(model, req.fileUrl, payload, {
pageRanges: req.pageRanges,
batchId: req.batchId,
signal,
});
}
return this.http.submitFile(model, req.filePath!, payload, {
pageRanges: req.pageRanges,
batchId: req.batchId,
signal,
});
}
private parseOCRResult(jobId: string, jsonlData: unknown[]): OCRResult {
const dataInfo: Record<string, unknown> = {};
const pages = jsonlData.flatMap((lineObj) => {
if (!isRecord(lineObj) || !isRecord(lineObj.result) || !Array.isArray(lineObj.result.ocrResults)) {
throw new ResultParseError("OCR result item is missing result.ocrResults.");
}
if (isRecord(lineObj.result.dataInfo)) {
Object.assign(dataInfo, lineObj.result.dataInfo);
}
return lineObj.result.ocrResults.map((item) => {
if (!isRecord(item) || !("prunedResult" in item)) {
throw new ResultParseError("OCR result page is missing prunedResult.");
}
return {
prunedResult: item.prunedResult,
ocrImageUrl: typeof item.ocrImage === "string" ? item.ocrImage : undefined,
docPreprocessingImageUrl: typeof item.docPreprocessingImage === "string" ? item.docPreprocessingImage : undefined,
inputImageUrl: typeof item.inputImage === "string" ? item.inputImage : undefined,
raw: item,
};
});
});
return { jobId, pages, dataInfo };
}
private parseDocParsingResult(jobId: string, jsonlData: unknown[]): DocParsingResult {
const dataInfo: Record<string, unknown> = {};
const pages = jsonlData.flatMap((lineObj) => {
if (!isRecord(lineObj) || !isRecord(lineObj.result) || !Array.isArray(lineObj.result.layoutParsingResults)) {
throw new ResultParseError("Document parsing result item is missing result.layoutParsingResults.");
}
if (isRecord(lineObj.result.dataInfo)) {
Object.assign(dataInfo, lineObj.result.dataInfo);
}
return lineObj.result.layoutParsingResults.map((item) => {
if (!isRecord(item) || !isRecord(item.markdown) || typeof item.markdown.text !== "string") {
throw new ResultParseError("Document parsing result page is missing markdown.text.");
}
return {
markdownText: item.markdown.text,
markdownImages: isRecord(item.markdown.images) ? stringMap(item.markdown.images) : {},
outputImages: isRecord(item.outputImages) ? stringMap(item.outputImages) : {},
prunedResult: item.prunedResult,
inputImageUrl: typeof item.inputImage === "string" ? item.inputImage : undefined,
exports: isRecord(item.exports) ? item.exports : {},
markdown: item.markdown,
raw: item,
};
});
});
return { jobId, pages, dataInfo };
}
private resolveJob(job: Job | string, expectedTask: Job["task"]): Job {
if (typeof job === "string") {
return {
jobId: job,
model: expectedTask === "ocr" ? Model.PPOCRv6 : Model.PaddleOCRVL16,
task: expectedTask,
};
}
if (job.task !== expectedTask) {
throw new InvalidRequestError(`Job ${job.jobId} is a ${job.task} job, not a ${expectedTask} job.`);
}
this.validateModelForTask(job.model, expectedTask);
return job;
}
private validateModelForTask(model: string, task: Job["task"]): void {
if (task === "ocr" && !isOCRModel(model)) {
throw new InvalidRequestError(`Model ${model} is not an OCR model.`);
}
if (task === "document_parsing" && !isDocumentParsingModel(model)) {
throw new InvalidRequestError(`Model ${model} is not a document parsing model.`);
}
}
}
function stringMap(value: Record<string, unknown>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, val] of Object.entries(value)) {
if (typeof val === "string") {
result[key] = val;
}
}
return result;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isDocParsingResult(result: OCRResult | DocParsingResult): result is DocParsingResult {
return result.pages.some((page) => "markdownText" in page);
}
function safeMapKeyFilename(key: string): string {
if (!key || key === "." || key === ".." || key.includes("/") || key.includes("\\") || key.startsWith(".")) {
throw new InvalidRequestError(`Unsafe resource filename: ${key}`);
}
return key;
}
function safeUrlBasename(url: URL): string {
const name = basename(url.pathname) || "resource";
if (name === "." || name === "..") {
return "resource";
}
return name;
}
function resourceExtension(resourceUrl: string): string {
try {
const url = new URL(resourceUrl);
return extname(url.pathname);
} catch {
return "";
}
}
+119
View File
@@ -0,0 +1,119 @@
// 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.
export class PaddleOCRAPIError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message);
this.name = "PaddleOCRAPIError";
this.cause = options?.cause;
}
}
export class AuthError extends PaddleOCRAPIError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "AuthError";
}
}
export class InvalidRequestError extends PaddleOCRAPIError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "InvalidRequestError";
}
}
export class APIError extends PaddleOCRAPIError {
statusCode: number;
constructor(statusCode: number, message: string, options?: ErrorOptions) {
super(`HTTP ${statusCode}: ${message}`, options);
this.name = "APIError";
this.statusCode = statusCode;
}
}
export class RateLimitError extends APIError {
constructor(message: string, options?: ErrorOptions) {
super(429, message, options);
this.name = "RateLimitError";
}
}
export class ServiceUnavailableError extends APIError {
constructor(statusCode: number, message: string, options?: ErrorOptions) {
super(statusCode, message, options);
this.name = "ServiceUnavailableError";
}
}
export class JobFailedError extends PaddleOCRAPIError {
jobId: string;
errorMsg: string;
constructor(jobId: string, errorMsg: string, options?: ErrorOptions) {
super(`Job ${jobId} failed: ${errorMsg}`, options);
this.name = "JobFailedError";
this.jobId = jobId;
this.errorMsg = errorMsg;
}
}
export class RequestTimeoutError extends PaddleOCRAPIError {
timeoutMs: number;
constructor(timeoutMs: number, options?: ErrorOptions) {
super(`Request timed out after ${timeoutMs}ms`, options);
this.name = "RequestTimeoutError";
this.timeoutMs = timeoutMs;
}
}
export class PollTimeoutError extends PaddleOCRAPIError {
jobId: string;
timeoutMs: number;
constructor(jobId: string, timeoutMs: number, options?: ErrorOptions) {
super(`Timed out after ${timeoutMs}ms waiting for job ${jobId}`, options);
this.name = "PollTimeoutError";
this.jobId = jobId;
this.timeoutMs = timeoutMs;
}
}
export class NetworkError extends PaddleOCRAPIError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "NetworkError";
}
}
export class FileNotFoundError extends PaddleOCRAPIError {
path: string;
constructor(path: string, options?: ErrorOptions) {
super(`File not found: ${path}`, options);
this.name = "FileNotFoundError";
this.path = path;
}
}
export class ResponseFormatError extends PaddleOCRAPIError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "ResponseFormatError";
}
}
export class ResultParseError extends PaddleOCRAPIError {
constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = "ResultParseError";
}
}
+51
View File
@@ -0,0 +1,51 @@
// 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.
export { PaddleOCRClient } from "./client.js";
export { Model, isDocumentParsingModel, isOCRModel, isVLModel } from "./models.js";
export type {
ClientOptions,
DocParsingOptions,
DocParsingRequest,
OCROptions,
OCRRequest,
PaddleOCRVLOptions,
PPStructureV3Options,
SaveResourceOptions,
} from "./models.js";
export type {
BatchStatus,
DocParsingPage,
DocParsingResult,
Job,
JobStatus,
OCRPage,
OCRResult,
Progress,
} from "./results.js";
export {
APIError,
AuthError,
FileNotFoundError,
InvalidRequestError,
JobFailedError,
NetworkError,
PaddleOCRAPIError,
PollTimeoutError,
RateLimitError,
RequestTimeoutError,
ResponseFormatError,
ResultParseError,
ServiceUnavailableError,
} from "./errors.js";
+25
View File
@@ -0,0 +1,25 @@
// 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.
/** Value to reject/throw when an operation was aborted via `AbortSignal` (aligns with `fetch` + `DOMException`). */
export function userAbortReason(signal: AbortSignal): unknown {
return signal.reason !== undefined
? signal.reason
: new DOMException("The user aborted a request.", "AbortError");
}
export function throwIfAborted(signal?: AbortSignal): void {
if (!signal?.aborted) return;
throw userAbortReason(signal);
}
+255
View File
@@ -0,0 +1,255 @@
// 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.
import {
APIError,
AuthError,
InvalidRequestError,
NetworkError,
RateLimitError,
RequestTimeoutError,
ResponseFormatError,
ResultParseError,
ServiceUnavailableError,
} from "../errors.js";
import { userAbortReason } from "./abort.js";
const DEFAULT_BASE_URL = "https://paddleocr.aistudio-app.com";
const API_PATH = "/api/v2/ocr/jobs";
interface SubmitOptions {
pageRanges?: string;
batchId?: string;
signal?: AbortSignal;
}
interface APIResponse<T> {
code?: number;
msg?: string;
data: T;
}
interface SubmitResponse {
jobId: string;
}
export class HttpClient {
private baseUrl: string;
private jobsUrl: string;
private token: string;
private requestTimeout: number;
private fetchImpl: typeof fetch;
private clientPlatform?: string;
constructor(
token: string,
baseUrl: string = DEFAULT_BASE_URL,
requestTimeout: number = 300000,
fetchImpl: typeof fetch = fetch,
clientPlatform?: string,
) {
this.token = token;
this.baseUrl = baseUrl.replace(/\/+$/, "");
this.jobsUrl = `${this.baseUrl}${API_PATH}`;
this.requestTimeout = requestTimeout;
this.fetchImpl = fetchImpl;
this.clientPlatform = clientPlatform;
}
async submitUrl(model: string, fileUrl: string, optionalPayload: object, options: SubmitOptions = {}): Promise<string> {
const body: Record<string, unknown> = { fileUrl, model, optionalPayload };
if (options.pageRanges !== undefined) {
body.pageRanges = options.pageRanges;
}
if (options.batchId !== undefined) {
body.batchId = options.batchId;
}
const data = await this.fetchJson<SubmitResponse>(this.jobsUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}, options.signal);
return requireJobId(data);
}
async submitFile(model: string, filePath: string, optionalPayload: object, options: SubmitOptions = {}): Promise<string> {
const fs = await import("fs");
const path = await import("path");
if (!fs.existsSync(filePath)) {
const { FileNotFoundError } = await import("../errors.js");
throw new FileNotFoundError(filePath);
}
const form = new FormData();
form.append("model", model);
form.append("optionalPayload", JSON.stringify(optionalPayload));
if (options.pageRanges !== undefined) {
form.append("pageRanges", options.pageRanges);
}
if (options.batchId !== undefined) {
form.append("batchId", options.batchId);
}
const fileBuffer = fs.readFileSync(filePath);
const blob = new Blob([fileBuffer]);
form.append("file", blob, path.basename(filePath));
const data = await this.fetchJson<SubmitResponse>(this.jobsUrl, {
method: "POST",
body: form,
}, options.signal);
return requireJobId(data);
}
async getJobStatus(jobId: string, signal?: AbortSignal, timeoutMs?: number): Promise<unknown> {
return this.fetchJson<unknown>(
`${this.jobsUrl}/${encodeURIComponent(jobId)}`,
{ method: "GET" },
signal,
true,
timeoutMs,
);
}
async getBatchStatus(batchId: string, signal?: AbortSignal, timeoutMs?: number): Promise<unknown> {
return this.fetchJson<unknown>(
`${this.jobsUrl}/batch/${encodeURIComponent(batchId)}`,
{ method: "GET" },
signal,
true,
timeoutMs,
);
}
async fetchJsonl(url: string, signal?: AbortSignal, timeoutMs?: number): Promise<unknown[]> {
const resp = await this.fetch(url, { method: "GET" }, signal, false, timeoutMs);
const text = await resp.text();
try {
return text
.trim()
.split("\n")
.filter((line) => line.trim())
.map((line) => JSON.parse(line) as unknown);
} catch (error) {
throw new ResultParseError("Failed to parse JSONL result payload.", { cause: error });
}
}
async fetchResource(url: string, signal?: AbortSignal, timeoutMs?: number): Promise<ArrayBuffer> {
const resp = await this.fetch(url, { method: "GET" }, signal, false, timeoutMs);
return resp.arrayBuffer();
}
private async fetchJson<T>(
url: string,
init: RequestInit,
signal?: AbortSignal,
withAuth: boolean = true,
timeoutMs?: number,
): Promise<T> {
const resp = await this.fetch(url, init, signal, withAuth, timeoutMs);
let payload: APIResponse<T>;
try {
payload = await resp.json() as APIResponse<T>;
} catch (error) {
throw new ResponseFormatError("Expected a JSON response body.", { cause: error });
}
if (payload.code !== undefined && payload.code !== 0) {
throw new APIError(resp.status, payload.msg || "PaddleOCR official API request failed.");
}
if (!payload || typeof payload !== "object" || !("data" in payload)) {
throw new ResponseFormatError("Response body is missing data.");
}
return payload.data;
}
private async fetch(
url: string,
init: RequestInit,
signal?: AbortSignal,
withAuth: boolean = true,
timeoutMs?: number,
): Promise<Response> {
const headers: Record<string, string> = {
...(init.headers as Record<string, string> || {}),
};
if (withAuth) {
headers.Authorization = `Bearer ${this.token}`;
if (this.clientPlatform) {
headers["Client-Platform"] = this.clientPlatform;
}
}
let resp: Response;
const timeoutController = new AbortController();
const effectiveTimeout = Math.max(0, Math.min(this.requestTimeout, timeoutMs ?? this.requestTimeout));
const timeoutID = setTimeout(() => timeoutController.abort(), effectiveTimeout);
const abortController = new AbortController();
const abort = () => abortController.abort();
timeoutController.signal.addEventListener("abort", abort, { once: true });
if (signal?.aborted) {
abort();
} else {
signal?.addEventListener("abort", abort, { once: true });
}
try {
resp = await this.fetchImpl(url, {
...init,
headers,
signal: abortController.signal,
});
} catch (e: unknown) {
if (signal?.aborted) {
throw userAbortReason(signal);
}
if (timeoutController.signal.aborted) {
throw new RequestTimeoutError(effectiveTimeout, { cause: e });
}
const message = e instanceof Error ? e.message : String(e);
throw new NetworkError(`Connection failed: ${message}`);
} finally {
clearTimeout(timeoutID);
signal?.removeEventListener("abort", abort);
}
if (resp.ok) return resp;
let text = await resp.text();
try {
const payload = JSON.parse(text) as { msg?: string; message?: string; errorMsg?: string };
text = payload.msg || payload.message || payload.errorMsg || text;
} catch {
// Keep raw response text.
}
if (resp.status === 401 || resp.status === 403) {
throw new AuthError(`Authentication failed: ${text}`);
} else if (resp.status === 400) {
throw new InvalidRequestError(`Bad request: ${text}`);
} else if (resp.status === 429) {
throw new RateLimitError(`Rate limit exceeded: ${text}`);
} else if (resp.status === 503 || resp.status === 504) {
throw new ServiceUnavailableError(resp.status, `Service unavailable: ${text}`);
} else {
throw new APIError(resp.status, text);
}
}
}
function requireJobId(data: SubmitResponse): string {
if (!data || typeof data.jobId !== "string" || data.jobId.length === 0) {
throw new ResponseFormatError("Submit response is missing jobId.");
}
return data.jobId;
}
+162
View File
@@ -0,0 +1,162 @@
// 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.
import { JobFailedError, PollTimeoutError, RequestTimeoutError, ResponseFormatError } from "../errors.js";
import type { BatchStatus, JobStatus, Progress } from "../results.js";
import { throwIfAborted, userAbortReason } from "./abort.js";
import { HttpClient } from "./http.js";
const INITIAL_INTERVAL = 3000;
const MULTIPLIER = 1.5;
const MAX_INTERVAL = 15000;
const MAX_WAIT_TIME = 600000;
export class Poller {
private http: HttpClient;
private maxWaitTime: number;
constructor(http: HttpClient, maxWaitTime: number = MAX_WAIT_TIME) {
this.http = http;
this.maxWaitTime = maxWaitTime;
}
async pollUntilDone(jobId: string, signal?: AbortSignal): Promise<unknown[]> {
let interval = INITIAL_INTERVAL;
const deadline = Date.now() + this.maxWaitTime;
while (Date.now() < deadline) {
throwIfAborted(signal);
const remaining = deadline - Date.now();
const data = await this.withPollTimeout(jobId, remaining, () => this.http.getJobStatus(jobId, signal, remaining));
const status = normalizeStatus(jobId, data);
if (status.state === "done") {
const jsonUrl = resultJsonUrl(data);
const resultRemaining = deadline - Date.now();
return await this.withPollTimeout(jobId, resultRemaining, () => this.http.fetchJsonl(jsonUrl, signal, resultRemaining));
}
if (status.state === "failed") {
throw new JobFailedError(jobId, status.errorMsg || "Unknown error");
}
await this.sleep(Math.min(interval, Math.max(0, deadline - Date.now())), signal);
interval = Math.min(interval * MULTIPLIER, MAX_INTERVAL);
}
throw new PollTimeoutError(jobId, this.maxWaitTime);
}
async getStatus(jobId: string, signal?: AbortSignal): Promise<JobStatus> {
return normalizeStatus(jobId, await this.http.getJobStatus(jobId, signal));
}
async getBatchStatus(batchId: string, signal?: AbortSignal): Promise<BatchStatus> {
const data = await this.http.getBatchStatus(batchId, signal);
if (!isRecord(data) || !Array.isArray(data.extractResult)) {
throw new ResponseFormatError("Batch response is missing extractResult.");
}
return {
batchId,
jobs: data.extractResult.map((item) => {
if (!isRecord(item) || typeof item.jobId !== "string") {
throw new ResponseFormatError("Batch extractResult item is missing jobId.");
}
return normalizeStatus(item.jobId, item);
}),
};
}
private sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
if (!signal) return;
signal.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(userAbortReason(signal));
},
{ once: true },
);
});
}
private async withPollTimeout<T>(jobId: string, remainingMs: number, operation: () => Promise<T>): Promise<T> {
if (remainingMs <= 0) {
throw new PollTimeoutError(jobId, this.maxWaitTime);
}
try {
return await operation();
} catch (error) {
if (error instanceof RequestTimeoutError && error.timeoutMs === remainingMs) {
throw new PollTimeoutError(jobId, this.maxWaitTime, { cause: error });
}
throw error;
}
}
}
function normalizeStatus(jobId: string, data: unknown): JobStatus {
if (!isRecord(data) || typeof data.state !== "string") {
throw new ResponseFormatError("Status response is missing state.");
}
if (!["pending", "running", "done", "failed"].includes(data.state)) {
throw new ResponseFormatError(`Unknown job state: ${data.state}`);
}
return {
jobId,
state: data.state as JobStatus["state"],
progress: normalizeProgress(data.extractProgress),
resultUrl: isRecord(data.resultUrl) ? stringMap(data.resultUrl) : undefined,
errorMsg: typeof data.errorMsg === "string" ? data.errorMsg : undefined,
};
}
function normalizeProgress(progress: unknown): Progress | undefined {
if (progress === undefined || progress === null) {
return undefined;
}
if (!isRecord(progress)) {
throw new ResponseFormatError("Status progress must be an object.");
}
return {
totalPages: typeof progress.totalPages === "number" ? progress.totalPages : 0,
extractedPages: typeof progress.extractedPages === "number" ? progress.extractedPages : 0,
startTime: typeof progress.startTime === "string" ? progress.startTime : undefined,
endTime: typeof progress.endTime === "string" ? progress.endTime : undefined,
};
}
function resultJsonUrl(data: unknown): string {
if (!isRecord(data) || !isRecord(data.resultUrl) || typeof data.resultUrl.jsonUrl !== "string") {
throw new ResponseFormatError("Done job response is missing resultUrl.jsonUrl.");
}
return data.resultUrl.jsonUrl;
}
function stringMap(value: Record<string, unknown>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, val] of Object.entries(value)) {
if (typeof val === "string") {
result[key] = val;
}
}
return result;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
+168
View File
@@ -0,0 +1,168 @@
// 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.
export enum Model {
PPOCRv5 = "PP-OCRv5",
PPOCRv5Latin = "PP-OCRv5-latin",
PPOCRv6 = "PP-OCRv6",
PPStructureV3 = "PP-StructureV3",
PaddleOCRVL = "PaddleOCR-VL",
PaddleOCRVL15 = "PaddleOCR-VL-1.5",
PaddleOCRVL16 = "PaddleOCR-VL-1.6",
}
const OCR_MODELS = new Set<string>([Model.PPOCRv5, Model.PPOCRv5Latin, Model.PPOCRv6]);
const DOCUMENT_PARSING_MODELS = new Set<string>([
Model.PPStructureV3,
Model.PaddleOCRVL,
Model.PaddleOCRVL15,
Model.PaddleOCRVL16,
]);
const VL_MODELS = new Set<string>([
Model.PaddleOCRVL,
Model.PaddleOCRVL15,
Model.PaddleOCRVL16,
]);
export function isOCRModel(
model: string
): model is Model.PPOCRv5 | Model.PPOCRv5Latin | Model.PPOCRv6 {
return OCR_MODELS.has(model);
}
export function isDocumentParsingModel(model: string): boolean {
return DOCUMENT_PARSING_MODELS.has(model);
}
export function isVLModel(model: string): boolean {
return VL_MODELS.has(model);
}
export interface OCROptions {
useDocOrientationClassify?: boolean;
useDocUnwarping?: boolean;
useTextlineOrientation?: boolean;
textDetLimitSideLen?: number;
textDetLimitType?: string;
textDetThresh?: number;
textDetBoxThresh?: number;
textDetUnclipRatio?: number;
textRecScoreThresh?: number;
visualize?: boolean;
[key: string]: unknown;
}
export interface PPStructureV3Options {
useDocOrientationClassify?: boolean;
useDocUnwarping?: boolean;
useTextlineOrientation?: boolean;
useSealRecognition?: boolean;
useTableRecognition?: boolean;
useFormulaRecognition?: boolean;
useChartRecognition?: boolean;
useRegionDetection?: boolean;
layoutThreshold?: number | Record<string, number>;
layoutNms?: boolean;
layoutUnclipRatio?: number | number[] | Record<string, number>;
layoutMergeBboxesMode?: string | Record<string, string>;
formatBlockContent?: boolean;
textDetLimitSideLen?: number;
textDetLimitType?: string;
textDetThresh?: number;
textDetBoxThresh?: number;
textDetUnclipRatio?: number;
textRecScoreThresh?: number;
useWiredTableCellsTransToHtml?: boolean;
useWirelessTableCellsTransToHtml?: boolean;
useTableOrientationClassify?: boolean;
useOcrResultsWithTableCells?: boolean;
useE2eWiredTableRecModel?: boolean;
useE2eWirelessTableRecModel?: boolean;
markdownIgnoreLabels?: string[];
prettifyMarkdown?: boolean;
showFormulaNumber?: boolean;
returnMarkdownImages?: boolean;
outputFormats?: string[];
visualize?: boolean;
[key: string]: unknown;
}
export interface PaddleOCRVLOptions {
useDocOrientationClassify?: boolean;
useDocUnwarping?: boolean;
useLayoutDetection?: boolean;
useChartRecognition?: boolean;
useSealRecognition?: boolean;
useOcrForImageBlock?: boolean;
layoutThreshold?: number | Record<string, number>;
layoutNms?: boolean;
layoutUnclipRatio?: number | number[] | Record<string, number>;
layoutMergeBboxesMode?: string | Record<string, string>;
layoutShapeMode?: "rect" | "quad" | "poly" | "auto";
promptLabel?: "ocr" | "formula" | "table" | "chart" | "seal" | "spotting";
formatBlockContent?: boolean;
repetitionPenalty?: number;
temperature?: number;
topP?: number;
minPixels?: number;
maxPixels?: number;
maxNewTokens?: number;
vlmExtraArgs?: Record<string, unknown>;
mergeLayoutBlocks?: boolean;
markdownIgnoreLabels?: string[];
prettifyMarkdown?: boolean;
showFormulaNumber?: boolean;
restructurePages?: boolean;
mergeTables?: boolean;
relevelTitles?: boolean;
returnMarkdownImages?: boolean;
outputFormats?: string[];
visualize?: boolean;
[key: string]: unknown;
}
export type DocParsingOptions = PPStructureV3Options | PaddleOCRVLOptions;
export interface OCRRequest {
model?: Model | string;
fileUrl?: string;
filePath?: string;
pageRanges?: string;
batchId?: string;
options?: OCROptions;
}
export interface DocParsingRequest {
model?: Model | string;
fileUrl?: string;
filePath?: string;
pageRanges?: string;
batchId?: string;
options?: DocParsingOptions;
}
export interface ClientOptions {
token?: string;
baseUrl?: string;
timeout?: number;
requestTimeout?: number;
pollTimeout?: number;
clientPlatform?: string;
fetch?: typeof fetch;
}
export interface SaveResourceOptions {
overwrite?: boolean;
filename?: string;
}
+72
View File
@@ -0,0 +1,72 @@
// 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.
export interface OCRPage {
prunedResult: unknown;
ocrImageUrl?: string;
docPreprocessingImageUrl?: string;
inputImageUrl?: string;
raw?: unknown;
}
export interface DocParsingPage {
markdownText: string;
markdownImages: Record<string, string>;
outputImages: Record<string, string>;
prunedResult?: unknown;
inputImageUrl?: string;
exports?: Record<string, unknown>;
markdown?: Record<string, unknown>;
raw?: unknown;
}
export interface OCRResult {
jobId: string;
pages: OCRPage[];
dataInfo?: Record<string, unknown>;
}
export interface DocParsingResult {
jobId: string;
pages: DocParsingPage[];
dataInfo?: Record<string, unknown>;
}
export interface Progress {
totalPages: number;
extractedPages: number;
startTime?: string;
endTime?: string;
}
export interface Job {
jobId: string;
model: string;
task: "ocr" | "document_parsing";
pageRanges?: string;
batchId?: string;
}
export interface JobStatus {
jobId: string;
state: "pending" | "running" | "done" | "failed";
progress?: Progress;
resultUrl?: Record<string, string>;
errorMsg?: string;
}
export interface BatchStatus {
batchId: string;
jobs: JobStatus[];
}
+703
View File
@@ -0,0 +1,703 @@
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
import {
APIError,
AuthError,
FileNotFoundError,
InvalidRequestError,
JobFailedError,
Model,
PaddleOCRClient,
PollTimeoutError,
RequestTimeoutError,
ResponseFormatError,
ResultParseError,
} from "../src/index.js";
import type { DocParsingResult, Job, OCRResult } from "../src/index.js";
type FetchCall = {
url: string;
init: RequestInit;
};
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function textResponse(body: string, status = 200): Response {
return new Response(body, { status });
}
function createClient(fetchImpl: typeof fetch, options: Partial<ConstructorParameters<typeof PaddleOCRClient>[0]> = {}) {
return new PaddleOCRClient({
token: "test-token",
baseUrl: "https://api.example.test",
requestTimeout: 100,
pollTimeout: 100,
fetch: fetchImpl,
...options,
});
}
function captureFetch(responses: Array<Response | Error | Promise<Response>>): { fetch: typeof fetch; calls: FetchCall[] } {
const calls: FetchCall[] = [];
const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(input), init: init ?? {} });
const next = responses.shift();
if (next instanceof Error) {
throw next;
}
if (!next) {
throw new Error("No mocked response remaining");
}
return next;
}) as unknown as typeof fetch;
return { fetch: fetchImpl, calls };
}
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
describe("PaddleOCRClient public contract", () => {
test("requires token at construction and reads PADDLEOCR_ACCESS_TOKEN fallback", () => {
const previous = process.env.PADDLEOCR_ACCESS_TOKEN;
delete process.env.PADDLEOCR_ACCESS_TOKEN;
expect(() => new PaddleOCRClient()).toThrow(AuthError);
process.env.PADDLEOCR_ACCESS_TOKEN = "env-token";
const { fetch } = captureFetch([jsonResponse({ data: { state: "running" } })]);
expect(() => new PaddleOCRClient({ baseUrl: "https://api.example.test", fetch })).not.toThrow();
if (previous === undefined) {
delete process.env.PADDLEOCR_ACCESS_TOKEN;
} else {
process.env.PADDLEOCR_ACCESS_TOKEN = previous;
}
});
test("exposes contract names and omits unpublished draft aliases", () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
expect(client.getStatus).toBeTypeOf("function");
expect(client.parseDocument).toBeTypeOf("function");
expect(client.submitDocumentParsing).toBeTypeOf("function");
expect(client.waitOcrResult).toBeTypeOf("function");
expect(client.waitDocumentParsingResult).toBeTypeOf("function");
expect(client.saveResource).toBeTypeOf("function");
expect(client.saveOcrResultResources).toBeTypeOf("function");
expect(client.saveDocumentParsingResultResources).toBeTypeOf("function");
expect("getResult" in client).toBe(false);
expect("waitForResult" in client).toBe(false);
expect("docParsing" in client).toBe(false);
expect("submitDocParsing" in client).toBe(false);
});
test("submitOcr sends contract body and returns job metadata", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-1" } })]);
const client = createClient(fetch);
const job = await client.submitOcr({
fileUrl: "https://files.example.test/invoice.pdf",
pageRanges: "1-2",
batchId: "batch-1",
options: { visualize: true },
});
expect(job).toEqual({
jobId: "job-1",
model: Model.PPOCRv6,
task: "ocr",
pageRanges: "1-2",
batchId: "batch-1",
});
expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs");
expect(calls[0].init.method).toBe("POST");
expect(calls[0].init.headers).toMatchObject({
Authorization: "Bearer test-token",
"Content-Type": "application/json",
});
expect(JSON.parse(String(calls[0].init.body))).toEqual({
fileUrl: "https://files.example.test/invoice.pdf",
model: Model.PPOCRv6,
optionalPayload: { visualize: true },
pageRanges: "1-2",
batchId: "batch-1",
});
});
test("clientPlatform is sent as Client-Platform header on API requests", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-1" } })]);
const client = createClient(fetch, { clientPlatform: "my-app" });
await client.submitOcr({
fileUrl: "https://files.example.test/invoice.pdf",
});
expect(calls[0].init.headers).toMatchObject({
"Client-Platform": "my-app",
});
});
test("submitOcr propagates explicit OCR model and defaults to PP-OCRv6", async () => {
const { fetch, calls } = captureFetch([
jsonResponse({ data: { jobId: "job-explicit" } }),
jsonResponse({ data: { jobId: "job-default" } }),
]);
const client = createClient(fetch);
const explicit = await client.submitOcr({
model: Model.PPOCRv5,
fileUrl: "https://files.example.test/explicit.pdf",
});
const implicit = await client.submitOcr({
fileUrl: "https://files.example.test/default.pdf",
});
expect(explicit.model).toBe(Model.PPOCRv5);
expect(implicit.model).toBe(Model.PPOCRv6);
expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv5);
expect(JSON.parse(String(calls[1].init.body)).model).toBe(Model.PPOCRv6);
});
test("submitOcr accepts PP-OCRv6 model name", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-v6" } })]);
const client = createClient(fetch);
const job = await client.submitOcr({
model: Model.PPOCRv6,
fileUrl: "https://files.example.test/v6.pdf",
});
expect(job.model).toBe(Model.PPOCRv6);
expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv6);
});
test("submitOcr accepts PP-OCRv5-latin model name", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-latin" } })]);
const client = createClient(fetch);
const job = await client.submitOcr({
model: Model.PPOCRv5Latin,
fileUrl: "https://files.example.test/latin.pdf",
});
expect(job.model).toBe(Model.PPOCRv5Latin);
expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PPOCRv5Latin);
});
test("submitOcr and submitDocumentParsing accept official model name strings", async () => {
const { fetch, calls } = captureFetch([
jsonResponse({ data: { jobId: "job-ocr" } }),
jsonResponse({ data: { jobId: "job-doc" } }),
]);
const client = createClient(fetch);
const ocrJob = await client.submitOcr({
model: "PP-OCRv5",
fileUrl: "https://files.example.test/ocr.pdf",
});
const docJob = await client.submitDocumentParsing({
model: "PaddleOCR-VL-1.6",
fileUrl: "https://files.example.test/doc.pdf",
});
expect(ocrJob.model).toBe("PP-OCRv5");
expect(docJob.model).toBe("PaddleOCR-VL-1.6");
expect(JSON.parse(String(calls[0].init.body)).model).toBe("PP-OCRv5");
expect(JSON.parse(String(calls[1].init.body)).model).toBe("PaddleOCR-VL-1.6");
});
test("model helpers classify current OCR and document parsing models", async () => {
const mod = await import("../src/index.js");
expect(mod.isOCRModel(Model.PPOCRv5)).toBe(true);
expect(mod.isOCRModel(Model.PPOCRv5Latin)).toBe(true);
expect(mod.isOCRModel(Model.PPOCRv6)).toBe(true);
expect(mod.isOCRModel(Model.PPStructureV3)).toBe(false);
expect(mod.isOCRModel("future-unknown-model")).toBe(false);
expect(mod.isDocumentParsingModel(Model.PaddleOCRVL)).toBe(true);
expect(mod.isDocumentParsingModel(Model.PaddleOCRVL16)).toBe(true);
expect(mod.isDocumentParsingModel(Model.PPOCRv5)).toBe(false);
});
test("submitDocumentParsing passes current and future document parsing options", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-doc" } })]);
const client = createClient(fetch);
await client.submitDocumentParsing({
model: Model.PaddleOCRVL16,
fileUrl: "https://files.example.test/doc.pdf",
options: {
useOcrForImageBlock: true,
formatBlockContent: true,
markdownIgnoreLabels: ["image"],
vlmExtraArgs: { temperature: 0.1 },
returnMarkdownImages: false,
outputFormats: ["docx"],
futureOption: "enabled",
},
});
expect(JSON.parse(String(calls[0].init.body)).optionalPayload).toEqual({
useOcrForImageBlock: true,
formatBlockContent: true,
markdownIgnoreLabels: ["image"],
vlmExtraArgs: { temperature: 0.1 },
returnMarkdownImages: false,
outputFormats: ["docx"],
futureOption: "enabled",
});
});
test("submitDocumentParsing defaults to PaddleOCR-VL-1.6", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { jobId: "job-doc" } })]);
const client = createClient(fetch);
const job = await client.submitDocumentParsing({
fileUrl: "https://files.example.test/doc.pdf",
});
expect(job.model).toBe(Model.PaddleOCRVL16);
expect(JSON.parse(String(calls[0].init.body)).model).toBe(Model.PaddleOCRVL16);
});
test("submitOcr rejects non-OCR models before network calls", async () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
await expect(
client.submitOcr({
model: Model.PPStructureV3,
fileUrl: "https://files.example.test/not-ocr.pdf",
}),
).rejects.toThrow(InvalidRequestError);
expect(fetch).not.toHaveBeenCalled();
});
test("validates submit requests before network calls", async () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
await expect(client.submitOcr({})).rejects.toThrow(InvalidRequestError);
await expect(
client.submitOcr({ fileUrl: "https://files.example.test/a.pdf", filePath: "./a.pdf" }),
).rejects.toThrow(InvalidRequestError);
expect(fetch).not.toHaveBeenCalled();
});
test("getStatus performs one non-blocking status request", async () => {
const { fetch, calls } = captureFetch([
jsonResponse({
data: {
state: "running",
extractProgress: { totalPages: 3, extractedPages: 1 },
},
}),
]);
const client = createClient(fetch);
await expect(client.getStatus("job-1")).resolves.toEqual({
jobId: "job-1",
state: "running",
progress: { totalPages: 3, extractedPages: 1 },
errorMsg: undefined,
});
expect(calls).toHaveLength(1);
expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs/job-1");
});
test("normalizes multiple trailing baseUrl slashes", async () => {
const { fetch, calls } = captureFetch([jsonResponse({ data: { state: "running" } })]);
const client = createClient(fetch, { baseUrl: "https://api.example.test///" });
await client.getStatus("job-1");
expect(calls[0].url).toBe("https://api.example.test/api/v2/ocr/jobs/job-1");
});
test("typed wait accepts bare jobId, fetches JSONL without Authorization, and parses OCR results", async () => {
const { fetch, calls } = captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job-1.jsonl" } } }),
textResponse(
JSON.stringify({
result: {
dataInfo: { numPages: 1 },
ocrResults: [
{
prunedResult: { text: "hello" },
ocrImage: "img.png",
docPreprocessingImage: "pre.png",
inputImage: "input.png",
},
],
},
}),
),
]);
const client = createClient(fetch);
await expect(client.waitOcrResult("job-1")).resolves.toMatchObject({
jobId: "job-1",
dataInfo: { numPages: 1 },
pages: [
{
prunedResult: { text: "hello" },
ocrImageUrl: "img.png",
docPreprocessingImageUrl: "pre.png",
inputImageUrl: "input.png",
raw: {
prunedResult: { text: "hello" },
ocrImage: "img.png",
docPreprocessingImage: "pre.png",
inputImage: "input.png",
},
},
],
});
expect(calls[1].url).toBe("https://storage.example.test/job-1.jsonl");
expect((calls[1].init.headers as Record<string, string>).Authorization).toBeUndefined();
});
test("typed waits reject mismatched Job task before polling", async () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
const docJob: Job = { jobId: "job-1", model: Model.PPStructureV3, task: "document_parsing" };
await expect(client.waitOcrResult(docJob)).rejects.toThrow(InvalidRequestError);
expect(fetch).not.toHaveBeenCalled();
});
test("typed waits reject mismatched Job model before polling", async () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
const ocrTaskWithDocumentModel: Job = { jobId: "job-1", model: Model.PPStructureV3, task: "ocr" };
const documentTaskWithOcrModel: Job = { jobId: "job-2", model: Model.PPOCRv5, task: "document_parsing" };
await expect(client.waitOcrResult(ocrTaskWithDocumentModel)).rejects.toThrow(InvalidRequestError);
await expect(client.waitDocumentParsingResult(documentTaskWithOcrModel)).rejects.toThrow(InvalidRequestError);
expect(fetch).not.toHaveBeenCalled();
});
test("polling maps failed, timeout, unknown state, and missing result URL", async () => {
const failed = createClient(captureFetch([jsonResponse({ data: { state: "failed", errorMsg: "boom" } })]).fetch);
await expect(failed.waitOcrResult("job-1")).rejects.toThrow(JobFailedError);
const unknown = createClient(captureFetch([jsonResponse({ data: { state: "paused" } })]).fetch);
await expect(unknown.waitOcrResult("job-1")).rejects.toThrow(ResponseFormatError);
const missingResult = createClient(captureFetch([jsonResponse({ data: { state: "done" } })]).fetch);
await expect(missingResult.waitOcrResult("job-1")).rejects.toThrow(ResponseFormatError);
vi.useFakeTimers();
const timeoutFetch = vi.fn(async () => jsonResponse({ data: { state: "running" } })) as unknown as typeof fetch;
const timeout = createClient(timeoutFetch, { pollTimeout: 1 });
const promise = expect(timeout.waitOcrResult("job-1")).rejects.toThrow(PollTimeoutError);
await vi.runAllTimersAsync();
await promise;
});
test("malformed successful responses and malformed JSONL use dedicated errors", async () => {
const malformedSubmit = createClient(captureFetch([jsonResponse({ data: {} })]).fetch);
await expect(malformedSubmit.submitOcr({ fileUrl: "https://files.example.test/a.pdf" })).rejects.toThrow(
ResponseFormatError,
);
const malformedJsonl = createClient(
captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse("{not-json}\n"),
]).fetch,
);
await expect(malformedJsonl.waitOcrResult("job-1")).rejects.toThrow(ResultParseError);
});
test("malformed fetched OCR result records use ResultParseError", async () => {
const client = createClient(
captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse(JSON.stringify({ notResult: {} })),
]).fetch,
);
await expect(client.waitOcrResult("job-1")).rejects.toThrow(ResultParseError);
});
test.each([
["missing ocrResults", { result: {} }],
["missing prunedResult", { result: { ocrResults: [{ ocrImage: "img.png" }] } }],
])("malformed OCR payload with %s uses ResultParseError", async (_name, payload) => {
const client = createClient(
captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse(JSON.stringify(payload)),
]).fetch,
);
await expect(client.waitOcrResult("job-1")).rejects.toThrow(ResultParseError);
});
test("waitDocumentParsingResult preserves raw page data and dataInfo", async () => {
const page = {
prunedResult: { blocks: [{ label: "text", content: "hello" }] },
markdown: { text: "hello", images: { "figure.png": "https://example.test/figure.png" }, isStart: true, isEnd: true },
outputImages: { "page.png": "https://example.test/page.png" },
inputImage: "https://example.test/input.png",
exports: { docx: "https://example.test/result.docx" },
};
const { fetch } = captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse(JSON.stringify({ result: { dataInfo: { numPages: 1 }, layoutParsingResults: [page] } })),
]);
const client = createClient(fetch);
await expect(client.waitDocumentParsingResult("job-1")).resolves.toMatchObject({
jobId: "job-1",
dataInfo: { numPages: 1 },
pages: [
{
markdownText: "hello",
markdownImages: { "figure.png": "https://example.test/figure.png" },
outputImages: { "page.png": "https://example.test/page.png" },
prunedResult: { blocks: [{ label: "text", content: "hello" }] },
inputImageUrl: "https://example.test/input.png",
exports: { docx: "https://example.test/result.docx" },
markdown: page.markdown,
raw: page,
},
],
});
});
test("malformed fetched document result records use ResultParseError", async () => {
const client = createClient(
captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse(JSON.stringify({ notResult: {} })),
]).fetch,
);
await expect(client.waitDocumentParsingResult("job-1")).rejects.toThrow(ResultParseError);
});
test.each([
["missing layoutParsingResults", { result: {} }],
["missing markdown", { result: { layoutParsingResults: [{}] } }],
["missing markdown.text", { result: { layoutParsingResults: [{ markdown: { images: {} } }] } }],
])("malformed document payload with %s uses ResultParseError", async (_name, payload) => {
const client = createClient(
captureFetch([
jsonResponse({ data: { state: "done", resultUrl: { jsonUrl: "https://storage.example.test/job.jsonl" } } }),
textResponse(JSON.stringify(payload)),
]).fetch,
);
await expect(client.waitDocumentParsingResult("job-1")).rejects.toThrow(ResultParseError);
});
test("HTTP status and timeout failures map to contract errors", async () => {
await expect(createClient(captureFetch([textResponse("bad auth", 401)]).fetch).getStatus("job-1")).rejects.toThrow(
AuthError,
);
await expect(createClient(captureFetch([textResponse("bad request", 400)]).fetch).getStatus("job-1")).rejects.toThrow(
InvalidRequestError,
);
await expect(createClient(captureFetch([textResponse("server error", 500)]).fetch).getStatus("job-1")).rejects.toThrow(
APIError,
);
const timeoutFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")));
}),
) as unknown as typeof fetch;
await expect(createClient(timeoutFetch, { requestTimeout: 1 }).getStatus("job-1")).rejects.toThrow(
RequestTimeoutError,
);
});
test("submitFile reports missing local files before network calls", async () => {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
await expect(client.submitOcr({ filePath: "/definitely/missing/file.pdf" })).rejects.toThrow(FileNotFoundError);
expect(fetch).not.toHaveBeenCalled();
});
test("saveResource writes URL downloads without auth and requires overwrite opt-in", async () => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const { fetch, calls } = captureFetch([textResponse("content"), textResponse("replacement")]);
const client = createClient(fetch);
const saved = await client.saveResource("https://storage.example.test/a/b/c.png", dir);
expect(saved).toBe(join(dir, "c.png"));
await expect(readFile(join(dir, "c.png"), "utf8")).resolves.toBe("content");
expect((calls[0].init.headers as Record<string, string>).Authorization).toBeUndefined();
await expect(client.saveResource("https://storage.example.test/a/b/c.png", dir)).rejects.toThrow(
InvalidRequestError,
);
await client.saveResource("https://storage.example.test/a/b/c.png", dir, { overwrite: true });
await expect(readFile(join(dir, "c.png"), "utf8")).resolves.toBe("replacement");
await expect(stat(join(dir, "c.png"))).resolves.toBeTruthy();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("saveOcrResultResources saves OCR result image URLs with stable page filenames", async () => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const { fetch, calls } = captureFetch([textResponse("page 1"), textResponse("page 2")]);
const client = createClient(fetch);
const result: OCRResult = {
jobId: "job-ocr",
pages: [
{ prunedResult: { text: "one" }, ocrImageUrl: "https://storage.example.test/results/page-a.png?sig=opaque" },
{ prunedResult: { text: "two" }, ocrImageUrl: "https://storage.example.test/results/page-b.jpg" },
],
};
const saved = await client.saveOcrResultResources(result, dir);
expect(saved).toEqual([join(dir, "ocr-page-1.png"), join(dir, "ocr-page-2.jpg")]);
await expect(readFile(join(dir, "ocr-page-1.png"), "utf8")).resolves.toBe("page 1");
await expect(readFile(join(dir, "ocr-page-2.jpg"), "utf8")).resolves.toBe("page 2");
expect(calls).toHaveLength(2);
expect(calls.map((call) => (call.init.headers as Record<string, string>).Authorization)).toEqual([
undefined,
undefined,
]);
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("saveDocumentParsingResultResources preserves safe document parsing map keys over opaque URL basenames", async () => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const { fetch } = captureFetch([textResponse("markdown image"), textResponse("output image")]);
const client = createClient(fetch);
const result: DocParsingResult = {
jobId: "job-doc",
pages: [
{
markdownText: "![figure](figure 1.png)",
markdownImages: {
"figure 1.png": "https://storage.example.test/download?id=markdown-image",
},
outputImages: {
"rendered-page.jpg": "https://storage.example.test/blob?id=output-image",
},
},
],
};
const saved = await client.saveDocumentParsingResultResources(result, dir);
expect(saved).toEqual([join(dir, "figure 1.png"), join(dir, "rendered-page.jpg")]);
await expect(readFile(join(dir, "figure 1.png"), "utf8")).resolves.toBe("markdown image");
await expect(readFile(join(dir, "rendered-page.jpg"), "utf8")).resolves.toBe("output image");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("saveDocumentParsingResultResources sorts document parsing map keys for deterministic output", async () => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const { fetch, calls } = captureFetch([textResponse("alpha"), textResponse("bravo"), textResponse("charlie")]);
const client = createClient(fetch);
const result: DocParsingResult = {
jobId: "job-doc",
pages: [
{
markdownText: "",
markdownImages: {
"charlie.png": "https://storage.example.test/charlie",
"alpha.png": "https://storage.example.test/alpha",
"bravo.png": "https://storage.example.test/bravo",
},
outputImages: {},
},
],
};
const saved = await client.saveDocumentParsingResultResources(result, dir);
expect(saved).toEqual([join(dir, "alpha.png"), join(dir, "bravo.png"), join(dir, "charlie.png")]);
expect(calls.map((call) => call.url)).toEqual([
"https://storage.example.test/alpha",
"https://storage.example.test/bravo",
"https://storage.example.test/charlie",
]);
await expect(readFile(join(dir, "alpha.png"), "utf8")).resolves.toBe("alpha");
await expect(readFile(join(dir, "bravo.png"), "utf8")).resolves.toBe("bravo");
await expect(readFile(join(dir, "charlie.png"), "utf8")).resolves.toBe("charlie");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test("saveOcrResultResources requires overwrite opt-in for bulk result files", async () => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const result: OCRResult = {
jobId: "job-ocr",
pages: [{ prunedResult: {}, ocrImageUrl: "https://storage.example.test/page.png" }],
};
await writeFile(join(dir, "ocr-page-1.png"), "existing");
const blocked = captureFetch([textResponse("unexpected")]);
await expect(createClient(blocked.fetch).saveOcrResultResources(result, dir)).rejects.toThrow(InvalidRequestError);
expect(blocked.calls).toHaveLength(0);
const replacement = captureFetch([textResponse("replacement")]);
await createClient(replacement.fetch).saveOcrResultResources(result, dir, { overwrite: true });
await expect(readFile(join(dir, "ocr-page-1.png"), "utf8")).resolves.toBe("replacement");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
test.each([
["empty", ""],
["absolute", "/absolute.png"],
["parent traversal", "../escape.png"],
["traversal segment", ".."],
["forward separator", "nested/escape.png"],
["backslash separator", "nested\\escape.png"],
])("saveDocumentParsingResultResources rejects unsafe document parsing map key: %s", async (_name, unsafeKey) => {
const dir = await mkdtemp(join(tmpdir(), "paddleocr-api-sdk-"));
try {
const { fetch } = captureFetch([]);
const client = createClient(fetch);
const result: DocParsingResult = {
jobId: "job-doc",
pages: [
{
markdownText: "",
markdownImages: {
[unsafeKey]: "https://storage.example.test/escape.png",
},
outputImages: {},
},
],
};
await expect(client.saveDocumentParsingResultResources(result, dir)).rejects.toThrow(InvalidRequestError);
expect(fetch).not.toHaveBeenCalled();
} finally {
await rm(dir, { recursive: true, force: true });
}
});
});
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"types": ["node"],
"outDir": "./dist",
"declaration": true,
"strict": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@paddleocr/api-sdk": ["src/index.ts"]
},
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests", "examples"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
splitting: false,
});
+1
View File
@@ -0,0 +1 @@
移步[docs](https://www.paddleocr.ai/v2.10.0/applications/overview.html)
+30
View File
@@ -0,0 +1,30 @@
## 😃 Awesome projects based on PaddleOCR
💗 PaddleOCR wouldnt be where it is today without its incredible community! A massive 🙌 thank you 🙌 to all our longtime partners, new collaborators, and everyone whos poured their passion into PaddleOCR — whether weve named you or not. Your support fuels our fire! 🔥
| Project Name | Description |
| ------------ | ----------- |
| [Umi-OCR](https://github.com/hiroi-sora/Umi-OCR) <a href="https://github.com/hiroi-sora/Umi-OCR"><img src="https://img.shields.io/github/stars/hiroi-sora/Umi-OCR"></a>|Free, Open-source, Batch Offline OCR Software.|
| [LearnOpenCV](http://github.com/spmallick/learnopencv) <a href="http://github.com/spmallick/learnopencv"><img src="https://img.shields.io/github/stars/spmallick/learnopencv"></a> | code for Computer Vision, Deep learning, and AI research articles.|
| [OmniParser](https://github.com/microsoft/OmniParser)<a href="https://github.com/microsoft/OmniParser"><img src="https://img.shields.io/github/stars/microsoft/OmniParser"></a> |OmniParser: Screen Parsing tool for Pure Vision Based GUI Agent.|
| [QAnything](https://github.com/netease-youdao/QAnything)<a href="https://github.com/netease-youdao/QAnything"><img src="https://img.shields.io/github/stars/netease-youdao/QAnything"></a> |Question and Answer based on Anything.|
| [PaddleHub](https://github.com/PaddlePaddle/PaddleHub)<a href="https://github.com/PaddlePaddle/PaddleHub"><img src="https://img.shields.io/github/stars/PaddlePaddle/PaddleHub"></a> |400+ AI Models: Rich, high-quality AI models, including CV, NLP, Speech, Video and Cross-Modal.|
| [PaddleNLP](https://github.com/PaddlePaddle/PaddleNLP)<a href="https://github.com/PaddlePaddle/PaddleNLP"><img src="https://img.shields.io/github/stars/PaddlePaddle/PaddleNLP"></a> |A Large Language Model (LLM) development suite based on the PaddlePaddle.|
| [Rerun](https://github.com/rerun-io/rerun) <a href="https://github.com/rerun-io/rerun"><img src="https://img.shields.io/github/stars/rerun-io/rerun"></a> | Rerun is building the multimodal data stack to model, ingest, store, query and view robotics-style data |
| [Dango-Translator](https://github.com/PantsuDango/Dango-Translator) <a href="https://github.com/PantsuDango/Dango-Translator"><img src="https://img.shields.io/github/stars/PantsuDango/Dango-Translator"></a> | Recognize text on the screen, translate it and show the translation results in real time.|
| [PDF-Extract-Kit](https://github.com/opendatalab/PDF-Extract-Kit) <a href="https://github.com/opendatalab/PDF-Extract-Kit"><img src="https://img.shields.io/github/stars/opendatalab/PDF-Extract-Kit"></a> | PDF-Extract-Kit is a powerful open-source toolkit designed to efficiently extract high-quality content from complex and diverse PDF documents. |
| [manga-image-translator](https://github.com/zyddnys/manga-image-translator) <a href="https://github.com/zyddnys/manga-image-translator"><img src="https://img.shields.io/github/stars/zyddnys/manga-image-translator"></a> | Translate texts in manga/images.|
| [March7thAssistant](https://github.com/moesnow/March7thAssistant) <a href="https://github.com/moesnow/March7thAssistant"><img src="https://img.shields.io/github/stars/moesnow/March7thAssistant"></a> | Daily Tasks: Stamina recovery, daily training, claiming rewards, commissions, and farming. |
| [PaddlePaddle/models](https://github.com/PaddlePaddle/models) <a href="https://github.com/PaddlePaddle/models"><img src="https://img.shields.io/github/stars/PaddlePaddle/models"></a> |PaddlePaddle's industrial-grade model zoo.|
| [katanaml/sparrow](https://github.com/katanaml/sparrow) <a href="https://github.com/katanaml/sparrow"><img src="https://img.shields.io/github/stars/katanaml/sparrow"></a> | Sparrow is an innovative open-source solution for efficient data extraction and processing from various documents and images. |
| [RapidOCR](https://github.com/RapidAI/RapidOCR) <a href="https://github.com/RapidAI/RapidOCR"><img src="https://img.shields.io/github/stars/RapidAI/RapidOCR"></a> | Awesome OCR multiple programming languages toolkits based on ONNXRuntime, OpenVINO, PaddlePaddle and PyTorch |
| [autoMate](https://github.com/yuruotong1/autoMate) <a href="https://github.com/yuruotong1/autoMate"><img src="https://img.shields.io/github/stars/yuruotong1/autoMate"></a> | AI-Powered Local Automation Tool & Let Your Computer Work for You. |
| [Agent-S](https://github.com/simular-ai/Agent-S) <a href="https://github.com/simular-ai/Agent-S"><img src="https://img.shields.io/github/stars/simular-ai/Agent-S"></a> | A Compositional Generalist-Specialist Framework for Computer Use Agents. |
| [pdf-craft](https://github.com/oomol-lab/pdf-craft) <a href="https://github.com/oomol-lab/pdf-craft"><img src="https://img.shields.io/github/stars/oomol-lab/pdf-craft"></a> | PDF Craft can convert PDF files into various other formats. |
| [VV](https://github.com/Cicada000/VV) <a href="https://github.com/Cicada000/VV"><img src="https://img.shields.io/github/stars/Cicada000/VV"></a> | Zhang Weiwei Quotations Search Project. |
| [docetl](https://github.com/ucbepic/docetl) <a href="https://github.com/ucbepic/docetl"><img src="https://img.shields.io/github/stars/ucbepic/docetl"></a> | DocETL is a tool for creating and executing data processing pipelines, especially suited for complex document processing tasks. |
| [ZenlessZoneZero-Auto](https://github.com/sMythicalBird/ZenlessZoneZero-Auto) <a href="https://github.com/sMythicalBird/ZenlessZoneZero-Auto"><img src="https://img.shields.io/github/stars/sMythicalBird/ZenlessZoneZero-Auto"></a> | Zenless Zone Zero Automation Framework. |
| [Yuxi-Know](https://github.com/xerrors/Yuxi-Know) <a href="https://github.com/xerrors/Yuxi-Know"><img src="https://img.shields.io/github/stars/xerrors/Yuxi-Know"></a> | Knowledge graph question answering system based on LLMs. |
| [PaddleSharp](https://github.com/sdcb/PaddleSharp) <a href="https://github.com/sdcb/PaddleSharp"><img src="https://img.shields.io/github/stars/sdcb/PaddleSharp"></a>|.NET/C# binding for Baidu paddle inference library and PaddleOCR |
| [python-office](https://github.com/CoderWanFeng/python-office) <a href="https://github.com/CoderWanFeng/python-office"><img src="https://img.shields.io/github/stars/CoderWanFeng/python-office"></a> | Python tool for office works. |
| [OnnxOCR](https://github.com/jingsongliujing/OnnxOCR) <a href="https://github.com/jingsongliujing/OnnxOCR"><img src="https://img.shields.io/github/stars/jingsongliujing/OnnxOCR"></a>|A lightweight OCR system based on PaddleOCR, decoupled from the PaddlePaddle deep learning training framework, with ultra-fast inference speed |
| [Frigate](https://github.com/blakeblackshear/frigate) <a href="https://github.com/blakeblackshear/frigate"><img src="https://img.shields.io/github/stars/blakeblackshear/frigate"></a> | Real-time NVR system with AI-powered object detection and License Plate Recognition (LPR) using PaddleOCR. |
| ... |... |
+2
View File
@@ -0,0 +1,2 @@
*.html linguist-language=python
*.ipynb linguist-language=python
+16
View File
@@ -0,0 +1,16 @@
.DS_Store
*.pth
*.pyc
*.pyo
*.log
*.tmp
*.pkl
__pycache__/
.idea/
output/
test/*.jpg
datasets/
index/
train_log/
log/
profiling_log/
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+132
View File
@@ -0,0 +1,132 @@
# Real-time Scene Text Detection with Differentiable Binarization
**note**: some code is inherited from [WenmuZhou/DBNet.pytorch](https://github.com/WenmuZhou/DBNet.pytorch)
[中文解读](https://zhuanlan.zhihu.com/p/94677957)
![network](imgs/paper/db.jpg)
## update
2020-06-07: 添加灰度图训练,训练灰度图时需要在配置里移除`dataset.args.transforms.Normalize`
## Install Using Conda
```
conda env create -f environment.yml
git clone https://github.com/WenmuZhou/DBNet.paddle.git
cd DBNet.paddle/
```
or
## Install Manually
```bash
conda create -n dbnet python=3.6
conda activate dbnet
conda install ipython pip
# python dependencies
pip install -r requirement.txt
# clone repo
git clone https://github.com/WenmuZhou/DBNet.paddle.git
cd DBNet.paddle/
```
## Requirements
* paddlepaddle 2.4+
## Download
TBD
## Data Preparation
Training data: prepare a text `train.txt` in the following format, use '\t' as a separator
```
./datasets/train/img/001.jpg ./datasets/train/gt/001.txt
```
Validation data: prepare a text `test.txt` in the following format, use '\t' as a separator
```
./datasets/test/img/001.jpg ./datasets/test/gt/001.txt
```
- Store images in the `img` folder
- Store groundtruth in the `gt` folder
The groundtruth can be `.txt` files, with the following format:
```
x1, y1, x2, y2, x3, y3, x4, y4, annotation
```
## Train
1. config the `dataset['train']['dataset'['data_path']'`,`dataset['validate']['dataset'['data_path']`in [config/icdar2015_resnet18_fpn_DBhead_polyLR.yaml](cconfig/icdar2015_resnet18_fpn_DBhead_polyLR.yaml)
* . single gpu train
```bash
bash single_gpu_train.sh
```
* . Multi-gpu training
```bash
bash multi_gpu_train.sh
```
## Test
[eval.py](tools/eval.py) is used to test model on test dataset
1. config `model_path` in [eval.sh](eval.sh)
2. use following script to test
```bash
bash eval.sh
```
## Predict
[predict.py](tools/predict.py) Can be used to inference on all images in a folder
1. config `model_path`,`input_folder`,`output_folder` in [predict.sh](predict.sh)
2. use following script to predict
```
bash predict.sh
```
You can change the `model_path` in the `predict.sh` file to your model location.
tips: if result is not good, you can change `thre` in [predict.sh](predict.sh)
## Export Model
[export_model.py](tools/export_model.py) Can be used to inference on all images in a folder
use following script to export inference model
```
python tools/export_model.py --config_file config/icdar2015_resnet50_FPN_DBhead_polyLR.yaml -o trainer.resume_checkpoint=model_best.pth trainer.output_dir=output/infer
```
## Paddle Inference infer
[infer.py](tools/infer.py) Can be used to inference on all images in a folder
use following script to export inference model
```
python tools/infer.py --model-dir=output/infer/ --img-path imgs/paper/db.jpg
```
<h2 id="Performance">Performance</h2>
### [ICDAR 2015](http://rrc.cvc.uab.es/?ch=4)
only train on ICDAR2015 dataset
| Method | image size (short size) |learning rate | Precision (%) | Recall (%) | F-measure (%) | FPS |
|:--------------------------:|:-------:|:--------:|:--------:|:------------:|:---------------:|:-----:|
| ImageNet-resnet50-FPN-DBHeadtorch |736 |1e-3|90.19 | 78.14 | 83.88 | 27 |
| ImageNet-resnet50-FPN-DBHeadpaddle |736 |1e-3| 89.47 | 79.03 | 83.92 | 27 |
| ImageNet-resnet50-FPN-DBHeadpaddle_amp |736 |1e-3| 88.62 | 79.95 | 84.06 | 27 |
### examples
TBD
### reference
1. https://arxiv.org/pdf/1911.08947.pdf
2. https://github.com/WenmuZhou/DBNet.pytorch
**If this repository helps youplease star it. Thanks.**
@@ -0,0 +1,2 @@
from .base_trainer import BaseTrainer
from .base_dataset import BaseDataSet
@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/4 13:12
# @Author : zhoujun
import copy
from paddle.io import Dataset
from data_loader.modules import *
class BaseDataSet(Dataset):
def __init__(
self,
data_path: str,
img_mode,
pre_processes,
filter_keys,
ignore_tags,
transform=None,
target_transform=None,
):
assert img_mode in ["RGB", "BRG", "GRAY"]
self.ignore_tags = ignore_tags
self.data_list = self.load_data(data_path)
item_keys = ["img_path", "img_name", "text_polys", "texts", "ignore_tags"]
for item in item_keys:
assert (
item in self.data_list[0]
), "data_list from load_data must contains {}".format(item_keys)
self.img_mode = img_mode
self.filter_keys = filter_keys
self.transform = transform
self.target_transform = target_transform
self._init_pre_processes(pre_processes)
def _init_pre_processes(self, pre_processes):
self.aug = []
if pre_processes is not None:
for aug in pre_processes:
if "args" not in aug:
args = {}
else:
args = aug["args"]
if isinstance(args, dict):
cls = eval(aug["type"])(**args)
else:
cls = eval(aug["type"])(args)
self.aug.append(cls)
def load_data(self, data_path: str) -> list:
"""
把数据加载为一个list:
:params data_path: 存储数据的文件夹或者文件
return a dict ,包含了,'img_path','img_name','text_polys','texts','ignore_tags'
"""
raise NotImplementedError
def apply_pre_processes(self, data):
for aug in self.aug:
data = aug(data)
return data
def __getitem__(self, index):
try:
data = copy.deepcopy(self.data_list[index])
im = cv2.imread(data["img_path"], 1 if self.img_mode != "GRAY" else 0)
if self.img_mode == "RGB":
im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
data["img"] = im
data["shape"] = [im.shape[0], im.shape[1]]
data = self.apply_pre_processes(data)
if self.transform:
data["img"] = self.transform(data["img"])
data["text_polys"] = data["text_polys"].tolist()
if len(self.filter_keys):
data_dict = {}
for k, v in data.items():
if k not in self.filter_keys:
data_dict[k] = v
return data_dict
else:
return data
except:
return self.__getitem__(np.random.randint(self.__len__()))
def __len__(self):
return len(self.data_list)
@@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:50
# @Author : zhoujun
import os
import pathlib
import shutil
from pprint import pformat
import anyconfig
import paddle
import numpy as np
import random
from paddle.jit import to_static
from paddle.static import InputSpec
from utils import setup_logger
class BaseTrainer:
def __init__(
self,
config,
model,
criterion,
train_loader,
validate_loader,
metric_cls,
post_process=None,
):
config["trainer"]["output_dir"] = os.path.join(
str(pathlib.Path(os.path.abspath(__name__)).parent),
config["trainer"]["output_dir"],
)
config["name"] = config["name"] + "_" + model.name
self.save_dir = config["trainer"]["output_dir"]
self.checkpoint_dir = os.path.join(self.save_dir, "checkpoint")
os.makedirs(self.checkpoint_dir, exist_ok=True)
self.global_step = 0
self.start_epoch = 0
self.config = config
self.criterion = criterion
# logger and tensorboard
self.visualdl_enable = self.config["trainer"].get("visual_dl", False)
self.epochs = self.config["trainer"]["epochs"]
self.log_iter = self.config["trainer"]["log_iter"]
if paddle.distributed.get_rank() == 0:
anyconfig.dump(config, os.path.join(self.save_dir, "config.yaml"))
self.logger = setup_logger(os.path.join(self.save_dir, "train.log"))
self.logger_info(pformat(self.config))
self.model = self.apply_to_static(model)
# device
if (
paddle.device.cuda.device_count() > 0
and paddle.device.is_compiled_with_cuda()
):
self.with_cuda = True
random.seed(self.config["trainer"]["seed"])
np.random.seed(self.config["trainer"]["seed"])
paddle.seed(self.config["trainer"]["seed"])
else:
self.with_cuda = False
self.logger_info("train with and paddle {}".format(paddle.__version__))
# metrics
self.metrics = {
"recall": 0,
"precision": 0,
"hmean": 0,
"train_loss": float("inf"),
"best_model_epoch": 0,
}
self.train_loader = train_loader
if validate_loader is not None:
assert post_process is not None and metric_cls is not None
self.validate_loader = validate_loader
self.post_process = post_process
self.metric_cls = metric_cls
self.train_loader_len = len(train_loader)
if self.validate_loader is not None:
self.logger_info(
"train dataset has {} samples,{} in dataloader, validate dataset has {} samples,{} in dataloader".format(
len(self.train_loader.dataset),
self.train_loader_len,
len(self.validate_loader.dataset),
len(self.validate_loader),
)
)
else:
self.logger_info(
"train dataset has {} samples,{} in dataloader".format(
len(self.train_loader.dataset), self.train_loader_len
)
)
self._initialize_scheduler()
self._initialize_optimizer()
# resume or finetune
if self.config["trainer"]["resume_checkpoint"] != "":
self._load_checkpoint(
self.config["trainer"]["resume_checkpoint"], resume=True
)
elif self.config["trainer"]["finetune_checkpoint"] != "":
self._load_checkpoint(
self.config["trainer"]["finetune_checkpoint"], resume=False
)
if self.visualdl_enable and paddle.distributed.get_rank() == 0:
from visualdl import LogWriter
self.writer = LogWriter(self.save_dir)
# 混合精度训练
self.amp = self.config.get("amp", None)
if self.amp == "None":
self.amp = None
if self.amp:
self.amp["scaler"] = paddle.amp.GradScaler(
init_loss_scaling=self.amp.get("scale_loss", 1024),
use_dynamic_loss_scaling=self.amp.get("use_dynamic_loss_scaling", True),
)
self.model, self.optimizer = paddle.amp.decorate(
models=self.model,
optimizers=self.optimizer,
level=self.amp.get("amp_level", "O2"),
)
# 分布式训练
if paddle.device.cuda.device_count() > 1:
self.model = paddle.DataParallel(self.model)
# make inverse Normalize
self.UN_Normalize = False
for t in self.config["dataset"]["train"]["dataset"]["args"]["transforms"]:
if t["type"] == "Normalize":
self.normalize_mean = t["args"]["mean"]
self.normalize_std = t["args"]["std"]
self.UN_Normalize = True
def apply_to_static(self, model):
support_to_static = self.config["trainer"].get("to_static", False)
if support_to_static:
specs = None
print("static")
specs = [InputSpec([None, 3, -1, -1])]
model = to_static(model, input_spec=specs)
self.logger_info(
"Successfully to apply @to_static with specs: {}".format(specs)
)
return model
def train(self):
"""
Full training logic
"""
for epoch in range(self.start_epoch + 1, self.epochs + 1):
self.epoch_result = self._train_epoch(epoch)
self._on_epoch_finish()
if paddle.distributed.get_rank() == 0 and self.visualdl_enable:
self.writer.close()
self._on_train_finish()
def _train_epoch(self, epoch):
"""
Training logic for an epoch
:param epoch: Current epoch number
"""
raise NotImplementedError
def _eval(self, epoch):
"""
eval logic for an epoch
:param epoch: Current epoch number
"""
raise NotImplementedError
def _on_epoch_finish(self):
raise NotImplementedError
def _on_train_finish(self):
raise NotImplementedError
def _save_checkpoint(self, epoch, file_name):
"""
Saving checkpoints
:param epoch: current epoch number
:param log: logging information of the epoch
:param save_best: if True, rename the saved checkpoint to 'model_best.pth.tar'
"""
state_dict = self.model.state_dict()
state = {
"epoch": epoch,
"global_step": self.global_step,
"state_dict": state_dict,
"optimizer": self.optimizer.state_dict(),
"config": self.config,
"metrics": self.metrics,
}
filename = os.path.join(self.checkpoint_dir, file_name)
paddle.save(state, filename)
def _load_checkpoint(self, checkpoint_path, resume):
"""
Resume from saved checkpoints
:param checkpoint_path: Checkpoint path to be resumed
"""
self.logger_info("Loading checkpoint: {} ...".format(checkpoint_path))
checkpoint = paddle.load(checkpoint_path)
self.model.set_state_dict(checkpoint["state_dict"])
if resume:
self.global_step = checkpoint["global_step"]
self.start_epoch = checkpoint["epoch"]
self.config["lr_scheduler"]["args"]["last_epoch"] = self.start_epoch
# self.scheduler.load_state_dict(checkpoint['scheduler'])
self.optimizer.set_state_dict(checkpoint["optimizer"])
if "metrics" in checkpoint:
self.metrics = checkpoint["metrics"]
self.logger_info(
"resume from checkpoint {} (epoch {})".format(
checkpoint_path, self.start_epoch
)
)
else:
self.logger_info("finetune from checkpoint {}".format(checkpoint_path))
def _initialize(self, name, module, *args, **kwargs):
module_name = self.config[name]["type"]
module_args = self.config[name].get("args", {})
assert all(
[k not in module_args for k in kwargs]
), "Overwriting kwargs given in config file is not allowed"
module_args.update(kwargs)
return getattr(module, module_name)(*args, **module_args)
def _initialize_scheduler(self):
self.lr_scheduler = self._initialize("lr_scheduler", paddle.optimizer.lr)
def _initialize_optimizer(self):
self.optimizer = self._initialize(
"optimizer",
paddle.optimizer,
parameters=self.model.parameters(),
learning_rate=self.lr_scheduler,
)
def inverse_normalize(self, batch_img):
if self.UN_Normalize:
batch_img[:, 0, :, :] = (
batch_img[:, 0, :, :] * self.normalize_std[0] + self.normalize_mean[0]
)
batch_img[:, 1, :, :] = (
batch_img[:, 1, :, :] * self.normalize_std[1] + self.normalize_mean[1]
)
batch_img[:, 2, :, :] = (
batch_img[:, 2, :, :] * self.normalize_std[2] + self.normalize_mean[2]
)
def logger_info(self, s):
if paddle.distributed.get_rank() == 0:
self.logger.info(s)
@@ -0,0 +1,40 @@
name: DBNet
dataset:
train:
dataset:
type: SynthTextDataset # 数据集类型
args:
data_path: ''# SynthTextDataset 根目录
pre_processes: # 数据的预处理过程,包含augment和标签制作
- type: IaaAugment # 使用imgaug进行变换
args:
- {'type':Fliplr, 'args':{'p':0.5}}
- {'type': Affine, 'args':{'rotate':[-10,10]}}
- {'type':Resize,'args':{'size':[0.5,3]}}
- type: EastRandomCropData
args:
size: [640,640]
max_tries: 50
keep_ratio: true
- type: MakeBorderMap
args:
shrink_ratio: 0.4
- type: MakeShrinkMap
args:
shrink_ratio: 0.4
min_text_size: 8
transforms: # 对图片进行的变换方式
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
filter_keys: ['img_path','img_name','text_polys','texts','ignore_tags','shape'] # 返回数据之前,从数据字典里删除的key
ignore_tags: ['*', '###']
loader:
batch_size: 1
shuffle: true
num_workers: 0
collate_fn: ''
@@ -0,0 +1,65 @@
name: DBNet
base: ['config/SynthText.yaml']
arch:
type: Model
backbone:
type: resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 10
show_images_iter: 50
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path: ./datasets/SynthText
img_mode: RGB
loader:
batch_size: 2
shuffle: true
num_workers: 6
collate_fn: ''
@@ -0,0 +1,69 @@
name: DBNet
dataset:
train:
dataset:
type: ICDAR2015Dataset # 数据集类型
args:
data_path: # 一个存放 img_path \t gt_path的文件
- ''
pre_processes: # 数据的预处理过程,包含augment和标签制作
- type: IaaAugment # 使用imgaug进行变换
args:
- {'type':Fliplr, 'args':{'p':0.5}}
- {'type': Affine, 'args':{'rotate':[-10,10]}}
- {'type':Resize,'args':{'size':[0.5,3]}}
- type: EastRandomCropData
args:
size: [640,640]
max_tries: 50
keep_ratio: true
- type: MakeBorderMap
args:
shrink_ratio: 0.4
thresh_min: 0.3
thresh_max: 0.7
- type: MakeShrinkMap
args:
shrink_ratio: 0.4
min_text_size: 8
transforms: # 对图片进行的变换方式
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
filter_keys: [img_path,img_name,text_polys,texts,ignore_tags,shape] # 返回数据之前,从数据字典里删除的key
ignore_tags: ['*', '###']
loader:
batch_size: 1
shuffle: true
num_workers: 0
collate_fn: ''
validate:
dataset:
type: ICDAR2015Dataset
args:
data_path:
- ''
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
transforms:
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
filter_keys: []
ignore_tags: ['*', '###']
loader:
batch_size: 1
shuffle: true
num_workers: 0
collate_fn: ICDARCollectFN
@@ -0,0 +1,82 @@
name: DBNet
base: ['config/icdar2015.yaml']
arch:
type: Model
backbone:
type: deformable_resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 10
show_images_iter: 50
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.txt
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.txt
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,82 @@
name: DBNet
base: ['config/icdar2015.yaml']
arch:
type: Model
backbone:
type: resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 10
show_images_iter: 50
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.txt
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.txt
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,83 @@
name: DBNet
base: ['config/icdar2015.yaml']
arch:
type: Model
backbone:
type: resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: StepLR
args:
step_size: 10
gama: 0.8
trainer:
seed: 2
epochs: 500
log_iter: 10
show_images_iter: 50
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.txt
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.txt
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,79 @@
name: DBNet
base: ['config/icdar2015.yaml']
arch:
type: Model
backbone:
type: resnet50
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
lr_scheduler:
type: Polynomial
args:
learning_rate: 0.001
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 10
show_images_iter: 50
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output/fp16_o2
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.txt
img_mode: RGB
loader:
batch_size: 16
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.txt
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,73 @@
name: DBNet
dataset:
train:
dataset:
type: DetDataset # 数据集类型
args:
data_path: # 一个存放 img_path \t gt_path的文件
- ''
pre_processes: # 数据的预处理过程,包含augment和标签制作
- type: IaaAugment # 使用imgaug进行变换
args:
- {'type':Fliplr, 'args':{'p':0.5}}
- {'type': Affine, 'args':{'rotate':[-10,10]}}
- {'type':Resize,'args':{'size':[0.5,3]}}
- type: EastRandomCropData
args:
size: [640,640]
max_tries: 50
keep_ratio: true
- type: MakeBorderMap
args:
shrink_ratio: 0.4
thresh_min: 0.3
thresh_max: 0.7
- type: MakeShrinkMap
args:
shrink_ratio: 0.4
min_text_size: 8
transforms: # 对图片进行的变换方式
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
load_char_annotation: false
expand_one_char: false
filter_keys: [img_path,img_name,text_polys,texts,ignore_tags,shape] # 返回数据之前,从数据字典里删除的key
ignore_tags: ['*', '###']
loader:
batch_size: 1
shuffle: true
num_workers: 0
collate_fn: ''
validate:
dataset:
type: DetDataset
args:
data_path:
- ''
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
transforms:
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
load_char_annotation: false # 是否加载字符级标注
expand_one_char: false # 是否对只有一个字符的框进行宽度扩充,扩充后w = w+h
filter_keys: []
ignore_tags: ['*', '###']
loader:
batch_size: 1
shuffle: true
num_workers: 0
collate_fn: ICDARCollectFN
@@ -0,0 +1,86 @@
name: DBNet
base: ['config/open_dataset.yaml']
arch:
type: Model
backbone:
type: deformable_resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 1
show_images_iter: 1
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.json
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 2
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.json
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,86 @@
name: DBNet
base: ['config/open_dataset.yaml']
arch:
type: Model
backbone:
type: resnest50
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 1
show_images_iter: 1
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.json
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 2
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.json
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,93 @@
name: DBNet
base: ['config/open_dataset.yaml']
arch:
type: Model
backbone:
type: resnet18
pretrained: true
neck:
type: FPN
inner_channels: 256
head:
type: DBHead
out_channels: 2
k: 50
post_processing:
type: SegDetectorRepresenter
args:
thresh: 0.3
box_thresh: 0.7
max_candidates: 1000
unclip_ratio: 1.5 # from paper
metric:
type: QuadMetric
args:
is_output_polygon: false
loss:
type: DBLoss
alpha: 1
beta: 10
ohem_ratio: 3
optimizer:
type: Adam
args:
lr: 0.001
weight_decay: 0
amsgrad: true
lr_scheduler:
type: WarmupPolyLR
args:
warmup_epoch: 3
trainer:
seed: 2
epochs: 1200
log_iter: 1
show_images_iter: 1
resume_checkpoint: ''
finetune_checkpoint: ''
output_dir: output
visual_dl: false
amp:
scale_loss: 1024
amp_level: O2
custom_white_list: []
custom_black_list: ['exp', 'sigmoid', 'concat']
dataset:
train:
dataset:
args:
data_path:
- ./datasets/train.json
transforms: # 对图片进行的变换方式
- type: ToTensor
args: {}
- type: Normalize
args:
mean: [0.485, 0.456, 0.406]
std: [0.229, 0.224, 0.225]
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 2
shuffle: true
num_workers: 6
collate_fn: ''
validate:
dataset:
args:
data_path:
- ./datasets/test.json
pre_processes:
- type: ResizeShortSize
args:
short_size: 736
resize_text_polys: false
img_mode: RGB
load_char_annotation: false
expand_one_char: false
loader:
batch_size: 1
shuffle: true
num_workers: 6
collate_fn: ICDARCollectFN
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:52
# @Author : zhoujun
import copy
import PIL
import numpy as np
import paddle
from paddle.io import DataLoader, DistributedBatchSampler, BatchSampler
from paddle.vision import transforms
def get_dataset(data_path, module_name, transform, dataset_args):
"""
获取训练dataset
:param data_path: dataset文件列表,每个文件内以如下格式存储 ‘path/to/img\tlabel
:param module_name: 所使用的自定义dataset名称,目前只支持data_loaders.ImageDataset
:param transform: 该数据集使用的transforms
:param dataset_args: module_name的参数
:return: 如果data_path列表不为空,返回对于的ConcatDataset对象,否则None
"""
from . import dataset
s_dataset = getattr(dataset, module_name)(
transform=transform, data_path=data_path, **dataset_args
)
return s_dataset
def get_transforms(transforms_config):
tr_list = []
for item in transforms_config:
if "args" not in item:
args = {}
else:
args = item["args"]
cls = getattr(transforms, item["type"])(**args)
tr_list.append(cls)
tr_list = transforms.Compose(tr_list)
return tr_list
class ICDARCollectFN:
def __init__(self, *args, **kwargs):
pass
def __call__(self, batch):
data_dict = {}
to_tensor_keys = []
for sample in batch:
for k, v in sample.items():
if k not in data_dict:
data_dict[k] = []
if isinstance(v, (np.ndarray, paddle.Tensor, PIL.Image.Image)):
if k not in to_tensor_keys:
to_tensor_keys.append(k)
data_dict[k].append(v)
for k in to_tensor_keys:
data_dict[k] = paddle.stack(data_dict[k], 0)
return data_dict
def get_dataloader(module_config, distributed=False):
if module_config is None:
return None
config = copy.deepcopy(module_config)
dataset_args = config["dataset"]["args"]
if "transforms" in dataset_args:
img_transforms = get_transforms(dataset_args.pop("transforms"))
else:
img_transforms = None
# 创建数据集
dataset_name = config["dataset"]["type"]
data_path = dataset_args.pop("data_path")
if data_path == None:
return None
data_path = [x for x in data_path if x is not None]
if len(data_path) == 0:
return None
if (
"collate_fn" not in config["loader"]
or config["loader"]["collate_fn"] is None
or len(config["loader"]["collate_fn"]) == 0
):
config["loader"]["collate_fn"] = None
else:
config["loader"]["collate_fn"] = eval(config["loader"]["collate_fn"])()
_dataset = get_dataset(
data_path=data_path,
module_name=dataset_name,
transform=img_transforms,
dataset_args=dataset_args,
)
sampler = None
if distributed:
# 3)使用DistributedSampler
batch_sampler = DistributedBatchSampler(
dataset=_dataset,
batch_size=config["loader"].pop("batch_size"),
shuffle=config["loader"].pop("shuffle"),
)
else:
batch_sampler = BatchSampler(
dataset=_dataset,
batch_size=config["loader"].pop("batch_size"),
shuffle=config["loader"].pop("shuffle"),
)
loader = DataLoader(
dataset=_dataset, batch_sampler=batch_sampler, **config["loader"]
)
return loader
@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:54
# @Author : zhoujun
import pathlib
import os
import cv2
import numpy as np
import scipy.io as sio
from tqdm.auto import tqdm
from base import BaseDataSet
from utils import order_points_clockwise, get_datalist, load, expand_polygon
class ICDAR2015Dataset(BaseDataSet):
def __init__(
self,
data_path: str,
img_mode,
pre_processes,
filter_keys,
ignore_tags,
transform=None,
**kwargs,
):
super().__init__(
data_path, img_mode, pre_processes, filter_keys, ignore_tags, transform
)
def load_data(self, data_path: str) -> list:
data_list = get_datalist(data_path)
t_data_list = []
for img_path, label_path in data_list:
data = self._get_annotation(label_path)
if len(data["text_polys"]) > 0:
item = {"img_path": img_path, "img_name": pathlib.Path(img_path).stem}
item.update(data)
t_data_list.append(item)
else:
print("there is no suit bbox in {}".format(label_path))
return t_data_list
def _get_annotation(self, label_path: str) -> dict:
boxes = []
texts = []
ignores = []
with open(label_path, encoding="utf-8", mode="r") as f:
for line in f.readlines():
params = line.strip().strip("\ufeff").strip("\xef\xbb\xbf").split(",")
try:
box = order_points_clockwise(
np.array(list(map(float, params[:8]))).reshape(-1, 2)
)
if cv2.contourArea(box) > 0:
boxes.append(box)
label = params[8]
texts.append(label)
ignores.append(label in self.ignore_tags)
except:
print("load label failed on {}".format(label_path))
data = {
"text_polys": np.array(boxes),
"texts": texts,
"ignore_tags": ignores,
}
return data
class DetDataset(BaseDataSet):
def __init__(
self,
data_path: str,
img_mode,
pre_processes,
filter_keys,
ignore_tags,
transform=None,
**kwargs,
):
self.load_char_annotation = kwargs["load_char_annotation"]
self.expand_one_char = kwargs["expand_one_char"]
super().__init__(
data_path, img_mode, pre_processes, filter_keys, ignore_tags, transform
)
def load_data(self, data_path: str) -> list:
"""
从json文件中读取出 文本行的坐标和gt,字符的坐标和gt
:param data_path:
:return:
"""
data_list = []
for path in data_path:
content = load(path)
for gt in tqdm(content["data_list"], desc="read file {}".format(path)):
img_path = os.path.join(content["data_root"], gt["img_name"])
polygons = []
texts = []
illegibility_list = []
language_list = []
for annotation in gt["annotations"]:
if len(annotation["polygon"]) == 0 or len(annotation["text"]) == 0:
continue
if len(annotation["text"]) > 1 and self.expand_one_char:
annotation["polygon"] = expand_polygon(annotation["polygon"])
polygons.append(annotation["polygon"])
texts.append(annotation["text"])
illegibility_list.append(annotation["illegibility"])
language_list.append(annotation["language"])
if self.load_char_annotation:
for char_annotation in annotation["chars"]:
if (
len(char_annotation["polygon"]) == 0
or len(char_annotation["char"]) == 0
):
continue
polygons.append(char_annotation["polygon"])
texts.append(char_annotation["char"])
illegibility_list.append(char_annotation["illegibility"])
language_list.append(char_annotation["language"])
data_list.append(
{
"img_path": img_path,
"img_name": gt["img_name"],
"text_polys": np.array(polygons),
"texts": texts,
"ignore_tags": illegibility_list,
}
)
return data_list
class SynthTextDataset(BaseDataSet):
def __init__(
self,
data_path: str,
img_mode,
pre_processes,
filter_keys,
transform=None,
**kwargs,
):
self.transform = transform
self.dataRoot = pathlib.Path(data_path)
if not self.dataRoot.exists():
raise FileNotFoundError("Dataset folder is not exist.")
self.targetFilePath = self.dataRoot / "gt.mat"
if not self.targetFilePath.exists():
raise FileExistsError("Target file is not exist.")
targets = {}
sio.loadmat(
self.targetFilePath,
targets,
squeeze_me=True,
struct_as_record=False,
variable_names=["imnames", "wordBB", "txt"],
)
self.imageNames = targets["imnames"]
self.wordBBoxes = targets["wordBB"]
self.transcripts = targets["txt"]
super().__init__(data_path, img_mode, pre_processes, filter_keys, transform)
def load_data(self, data_path: str) -> list:
t_data_list = []
for imageName, wordBBoxes, texts in zip(
self.imageNames, self.wordBBoxes, self.transcripts
):
item = {}
wordBBoxes = (
np.expand_dims(wordBBoxes, axis=2)
if (wordBBoxes.ndim == 2)
else wordBBoxes
)
_, _, numOfWords = wordBBoxes.shape
text_polys = wordBBoxes.reshape(
[8, numOfWords], order="F"
).T # num_words * 8
text_polys = text_polys.reshape(numOfWords, 4, 2) # num_of_words * 4 * 2
transcripts = [word for line in texts for word in line.split()]
if numOfWords != len(transcripts):
continue
item["img_path"] = str(self.dataRoot / imageName)
item["img_name"] = (self.dataRoot / imageName).stem
item["text_polys"] = text_polys
item["texts"] = transcripts
item["ignore_tags"] = [x in self.ignore_tags for x in transcripts]
t_data_list.append(item)
return t_data_list
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/4 10:53
# @Author : zhoujun
from .iaa_augment import IaaAugment
from .augment import *
from .random_crop_data import EastRandomCropData, PSERandomCrop
from .make_border_map import MakeBorderMap
from .make_shrink_map import MakeShrinkMap
@@ -0,0 +1,308 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:52
# @Author : zhoujun
import math
import numbers
import random
import cv2
import numpy as np
from skimage.util import random_noise
class RandomNoise:
def __init__(self, random_rate):
self.random_rate = random_rate
def __call__(self, data: dict):
"""
对图片加噪声
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
data["img"] = (
random_noise(data["img"], mode="gaussian", clip=True) * 255
).astype(data["img"].dtype)
return data
class RandomScale:
def __init__(self, scales, random_rate):
"""
:param scales: 尺度
:param random_rate: 随机系数
:return:
"""
self.random_rate = random_rate
self.scales = scales
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
im = data["img"]
text_polys = data["text_polys"]
tmp_text_polys = text_polys.copy()
rd_scale = float(np.random.choice(self.scales))
im = cv2.resize(im, dsize=None, fx=rd_scale, fy=rd_scale)
tmp_text_polys *= rd_scale
data["img"] = im
data["text_polys"] = tmp_text_polys
return data
class RandomRotateImgBox:
def __init__(self, degrees, random_rate, same_size=False):
"""
:param degrees: 角度,可以是一个数值或者list
:param random_rate: 随机系数
:param same_size: 是否保持和原图一样大
:return:
"""
if isinstance(degrees, numbers.Number):
if degrees < 0:
raise ValueError("If degrees is a single number, it must be positive.")
degrees = (-degrees, degrees)
elif (
isinstance(degrees, list)
or isinstance(degrees, tuple)
or isinstance(degrees, np.ndarray)
):
if len(degrees) != 2:
raise ValueError("If degrees is a sequence, it must be of len 2.")
degrees = degrees
else:
raise Exception("degrees must in Number or list or tuple or np.ndarray")
self.degrees = degrees
self.same_size = same_size
self.random_rate = random_rate
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
im = data["img"]
text_polys = data["text_polys"]
# ---------------------- 旋转图像 ----------------------
w = im.shape[1]
h = im.shape[0]
angle = np.random.uniform(self.degrees[0], self.degrees[1])
if self.same_size:
nw = w
nh = h
else:
# 角度变弧度
rangle = np.deg2rad(angle)
# 计算旋转之后图像的w, h
nw = abs(np.sin(rangle) * h) + abs(np.cos(rangle) * w)
nh = abs(np.cos(rangle) * h) + abs(np.sin(rangle) * w)
# 构造仿射矩阵
rot_mat = cv2.getRotationMatrix2D((nw * 0.5, nh * 0.5), angle, 1)
# 计算原图中心点到新图中心点的偏移量
rot_move = np.dot(rot_mat, np.array([(nw - w) * 0.5, (nh - h) * 0.5, 0]))
# 更新仿射矩阵
rot_mat[0, 2] += rot_move[0]
rot_mat[1, 2] += rot_move[1]
# 仿射变换
rot_img = cv2.warpAffine(
im,
rot_mat,
(int(math.ceil(nw)), int(math.ceil(nh))),
flags=cv2.INTER_LANCZOS4,
)
# ---------------------- 矫正bbox坐标 ----------------------
# rot_mat是最终的旋转矩阵
# 获取原始bbox的四个中点,然后将这四个点转换到旋转后的坐标系下
rot_text_polys = list()
for bbox in text_polys:
point1 = np.dot(rot_mat, np.array([bbox[0, 0], bbox[0, 1], 1]))
point2 = np.dot(rot_mat, np.array([bbox[1, 0], bbox[1, 1], 1]))
point3 = np.dot(rot_mat, np.array([bbox[2, 0], bbox[2, 1], 1]))
point4 = np.dot(rot_mat, np.array([bbox[3, 0], bbox[3, 1], 1]))
rot_text_polys.append([point1, point2, point3, point4])
data["img"] = rot_img
data["text_polys"] = np.array(rot_text_polys)
return data
class RandomResize:
def __init__(self, size, random_rate, keep_ratio=False):
"""
:param input_size: resize尺寸,数字或者list的形式,如果为list形式,就是[w,h]
:param random_rate: 随机系数
:param keep_ratio: 是否保持长宽比
:return:
"""
if isinstance(size, numbers.Number):
if size < 0:
raise ValueError(
"If input_size is a single number, it must be positive."
)
size = (size, size)
elif (
isinstance(size, list)
or isinstance(size, tuple)
or isinstance(size, np.ndarray)
):
if len(size) != 2:
raise ValueError("If input_size is a sequence, it must be of len 2.")
size = (size[0], size[1])
else:
raise Exception("input_size must in Number or list or tuple or np.ndarray")
self.size = size
self.keep_ratio = keep_ratio
self.random_rate = random_rate
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
im = data["img"]
text_polys = data["text_polys"]
if self.keep_ratio:
# 将图片短边pad到和长边一样
h, w, c = im.shape
max_h = max(h, self.size[0])
max_w = max(w, self.size[1])
im_padded = np.zeros((max_h, max_w, c), dtype=np.uint8)
im_padded[:h, :w] = im.copy()
im = im_padded
text_polys = text_polys.astype(np.float32)
h, w, _ = im.shape
im = cv2.resize(im, self.size)
w_scale = self.size[0] / float(w)
h_scale = self.size[1] / float(h)
text_polys[:, :, 0] *= w_scale
text_polys[:, :, 1] *= h_scale
data["img"] = im
data["text_polys"] = text_polys
return data
def resize_image(img, short_size):
height, width, _ = img.shape
if height < width:
new_height = short_size
new_width = new_height / height * width
else:
new_width = short_size
new_height = new_width / width * height
new_height = int(round(new_height / 32) * 32)
new_width = int(round(new_width / 32) * 32)
resized_img = cv2.resize(img, (new_width, new_height))
return resized_img, (new_width / width, new_height / height)
class ResizeShortSize:
def __init__(self, short_size, resize_text_polys=True):
"""
:param size: resize尺寸,数字或者list的形式,如果为list形式,就是[w,h]
:return:
"""
self.short_size = short_size
self.resize_text_polys = resize_text_polys
def __call__(self, data: dict) -> dict:
"""
对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
im = data["img"]
text_polys = data["text_polys"]
h, w, _ = im.shape
short_edge = min(h, w)
if short_edge < self.short_size:
# 保证短边 >= short_size
scale = self.short_size / short_edge
im = cv2.resize(im, dsize=None, fx=scale, fy=scale)
scale = (scale, scale)
# im, scale = resize_image(im, self.short_size)
if self.resize_text_polys:
# text_polys *= scale
text_polys[:, 0] *= scale[0]
text_polys[:, 1] *= scale[1]
data["img"] = im
data["text_polys"] = text_polys
return data
class HorizontalFlip:
def __init__(self, random_rate):
"""
:param random_rate: 随机系数
"""
self.random_rate = random_rate
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
im = data["img"]
text_polys = data["text_polys"]
flip_text_polys = text_polys.copy()
flip_im = cv2.flip(im, 1)
h, w, _ = flip_im.shape
flip_text_polys[:, :, 0] = w - flip_text_polys[:, :, 0]
data["img"] = flip_im
data["text_polys"] = flip_text_polys
return data
class VerticalFlip:
def __init__(self, random_rate):
"""
:param random_rate: 随机系数
"""
self.random_rate = random_rate
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
if random.random() > self.random_rate:
return data
im = data["img"]
text_polys = data["text_polys"]
flip_text_polys = text_polys.copy()
flip_im = cv2.flip(im, 0)
h, w, _ = flip_im.shape
flip_text_polys[:, :, 1] = h - flip_text_polys[:, :, 1]
data["img"] = flip_im
data["text_polys"] = flip_text_polys
return data
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/4 18:06
# @Author : zhoujun
import numpy as np
import imgaug
import imgaug.augmenters as iaa
class AugmenterBuilder(object):
def __init__(self):
pass
def build(self, args, root=True):
if args is None or len(args) == 0:
return None
elif isinstance(args, list):
if root:
sequence = [self.build(value, root=False) for value in args]
return iaa.Sequential(sequence)
else:
return getattr(iaa, args[0])(
*[self.to_tuple_if_list(a) for a in args[1:]]
)
elif isinstance(args, dict):
cls = getattr(iaa, args["type"])
return cls(**{k: self.to_tuple_if_list(v) for k, v in args["args"].items()})
else:
raise RuntimeError("unknown augmenter arg: " + str(args))
def to_tuple_if_list(self, obj):
if isinstance(obj, list):
return tuple(obj)
return obj
class IaaAugment:
def __init__(self, augmenter_args):
self.augmenter_args = augmenter_args
self.augmenter = AugmenterBuilder().build(self.augmenter_args)
def __call__(self, data):
image = data["img"]
shape = image.shape
if self.augmenter:
aug = self.augmenter.to_deterministic()
data["img"] = aug.augment_image(image)
data = self.may_augment_annotation(aug, data, shape)
return data
def may_augment_annotation(self, aug, data, shape):
if aug is None:
return data
line_polys = []
for poly in data["text_polys"]:
new_poly = self.may_augment_poly(aug, shape, poly)
line_polys.append(new_poly)
data["text_polys"] = np.array(line_polys)
return data
def may_augment_poly(self, aug, img_shape, poly):
keypoints = [imgaug.Keypoint(p[0], p[1]) for p in poly]
keypoints = aug.augment_keypoints(
[imgaug.KeypointsOnImage(keypoints, shape=img_shape)]
)[0].keypoints
poly = [(p.x, p.y) for p in keypoints]
return poly
@@ -0,0 +1,159 @@
import cv2
import numpy as np
np.seterr(divide="ignore", invalid="ignore")
import pyclipper
from shapely.geometry import Polygon
class MakeBorderMap:
def __init__(self, shrink_ratio=0.4, thresh_min=0.3, thresh_max=0.7):
self.shrink_ratio = shrink_ratio
self.thresh_min = thresh_min
self.thresh_max = thresh_max
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
im = data["img"]
text_polys = data["text_polys"]
ignore_tags = data["ignore_tags"]
canvas = np.zeros(im.shape[:2], dtype=np.float32)
mask = np.zeros(im.shape[:2], dtype=np.float32)
for i in range(len(text_polys)):
if ignore_tags[i]:
continue
self.draw_border_map(text_polys[i], canvas, mask=mask)
canvas = canvas * (self.thresh_max - self.thresh_min) + self.thresh_min
data["threshold_map"] = canvas
data["threshold_mask"] = mask
return data
def draw_border_map(self, polygon, canvas, mask):
polygon = np.array(polygon)
assert polygon.ndim == 2
assert polygon.shape[1] == 2
polygon_shape = Polygon(polygon)
if polygon_shape.area <= 0:
return
distance = (
polygon_shape.area
* (1 - np.power(self.shrink_ratio, 2))
/ polygon_shape.length
)
subject = [tuple(l) for l in polygon]
padding = pyclipper.PyclipperOffset()
padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
padded_polygon = np.array(padding.Execute(distance)[0])
cv2.fillPoly(mask, [padded_polygon.astype(np.int32)], 1.0)
xmin = padded_polygon[:, 0].min()
xmax = padded_polygon[:, 0].max()
ymin = padded_polygon[:, 1].min()
ymax = padded_polygon[:, 1].max()
width = xmax - xmin + 1
height = ymax - ymin + 1
polygon[:, 0] = polygon[:, 0] - xmin
polygon[:, 1] = polygon[:, 1] - ymin
xs = np.broadcast_to(
np.linspace(0, width - 1, num=width).reshape(1, width), (height, width)
)
ys = np.broadcast_to(
np.linspace(0, height - 1, num=height).reshape(height, 1), (height, width)
)
distance_map = np.zeros((polygon.shape[0], height, width), dtype=np.float32)
for i in range(polygon.shape[0]):
j = (i + 1) % polygon.shape[0]
absolute_distance = self.distance(xs, ys, polygon[i], polygon[j])
distance_map[i] = np.clip(absolute_distance / distance, 0, 1)
distance_map = distance_map.min(axis=0)
xmin_valid = min(max(0, xmin), canvas.shape[1] - 1)
xmax_valid = min(max(0, xmax), canvas.shape[1] - 1)
ymin_valid = min(max(0, ymin), canvas.shape[0] - 1)
ymax_valid = min(max(0, ymax), canvas.shape[0] - 1)
canvas[ymin_valid : ymax_valid + 1, xmin_valid : xmax_valid + 1] = np.fmax(
1
- distance_map[
ymin_valid - ymin : ymax_valid - ymax + height,
xmin_valid - xmin : xmax_valid - xmax + width,
],
canvas[ymin_valid : ymax_valid + 1, xmin_valid : xmax_valid + 1],
)
def distance(self, xs, ys, point_1, point_2):
"""
compute the distance from point to a line
ys: coordinates in the first axis
xs: coordinates in the second axis
point_1, point_2: (x, y), the end of the line
"""
height, width = xs.shape[:2]
square_distance_1 = np.square(xs - point_1[0]) + np.square(ys - point_1[1])
square_distance_2 = np.square(xs - point_2[0]) + np.square(ys - point_2[1])
square_distance = np.square(point_1[0] - point_2[0]) + np.square(
point_1[1] - point_2[1]
)
cosin = (square_distance - square_distance_1 - square_distance_2) / (
2 * np.sqrt(square_distance_1 * square_distance_2)
)
square_sin = 1 - np.square(cosin)
square_sin = np.nan_to_num(square_sin)
result = np.sqrt(
square_distance_1 * square_distance_2 * square_sin / square_distance
)
result[cosin < 0] = np.sqrt(np.fmin(square_distance_1, square_distance_2))[
cosin < 0
]
# self.extend_line(point_1, point_2, result)
return result
def extend_line(self, point_1, point_2, result):
ex_point_1 = (
int(
round(point_1[0] + (point_1[0] - point_2[0]) * (1 + self.shrink_ratio))
),
int(
round(point_1[1] + (point_1[1] - point_2[1]) * (1 + self.shrink_ratio))
),
)
cv2.line(
result,
tuple(ex_point_1),
tuple(point_1),
4096.0,
1,
lineType=cv2.LINE_AA,
shift=0,
)
ex_point_2 = (
int(
round(point_2[0] + (point_2[0] - point_1[0]) * (1 + self.shrink_ratio))
),
int(
round(point_2[1] + (point_2[1] - point_1[1]) * (1 + self.shrink_ratio))
),
)
cv2.line(
result,
tuple(ex_point_2),
tuple(point_2),
4096.0,
1,
lineType=cv2.LINE_AA,
shift=0,
)
return ex_point_1, ex_point_2
@@ -0,0 +1,129 @@
import numpy as np
import cv2
def shrink_polygon_py(polygon, shrink_ratio):
"""
对框进行缩放,返回去的比例为1/shrink_ratio 即可
"""
cx = polygon[:, 0].mean()
cy = polygon[:, 1].mean()
polygon[:, 0] = cx + (polygon[:, 0] - cx) * shrink_ratio
polygon[:, 1] = cy + (polygon[:, 1] - cy) * shrink_ratio
return polygon
def shrink_polygon_pyclipper(polygon, shrink_ratio):
from shapely.geometry import Polygon
import pyclipper
polygon_shape = Polygon(polygon)
distance = (
polygon_shape.area * (1 - np.power(shrink_ratio, 2)) / polygon_shape.length
)
subject = [tuple(l) for l in polygon]
padding = pyclipper.PyclipperOffset()
padding.AddPath(subject, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
shrunk = padding.Execute(-distance)
if shrunk == []:
shrunk = np.array(shrunk)
else:
shrunk = np.array(shrunk[0]).reshape(-1, 2)
return shrunk
class MakeShrinkMap:
r"""
Making binary mask from detection data with ICDAR format.
Typically following the process of class `MakeICDARData`.
"""
def __init__(self, min_text_size=8, shrink_ratio=0.4, shrink_type="pyclipper"):
shrink_func_dict = {
"py": shrink_polygon_py,
"pyclipper": shrink_polygon_pyclipper,
}
self.shrink_func = shrink_func_dict[shrink_type]
self.min_text_size = min_text_size
self.shrink_ratio = shrink_ratio
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
image = data["img"]
text_polys = data["text_polys"]
ignore_tags = data["ignore_tags"]
h, w = image.shape[:2]
text_polys, ignore_tags = self.validate_polygons(text_polys, ignore_tags, h, w)
gt = np.zeros((h, w), dtype=np.float32)
mask = np.ones((h, w), dtype=np.float32)
for i in range(len(text_polys)):
polygon = text_polys[i]
height = max(polygon[:, 1]) - min(polygon[:, 1])
width = max(polygon[:, 0]) - min(polygon[:, 0])
if ignore_tags[i] or min(height, width) < self.min_text_size:
cv2.fillPoly(mask, polygon.astype(np.int32)[np.newaxis, :, :], 0)
ignore_tags[i] = True
else:
shrunk = self.shrink_func(polygon, self.shrink_ratio)
if shrunk.size == 0:
cv2.fillPoly(mask, polygon.astype(np.int32)[np.newaxis, :, :], 0)
ignore_tags[i] = True
continue
cv2.fillPoly(gt, [shrunk.astype(np.int32)], 1)
data["shrink_map"] = gt
data["shrink_mask"] = mask
return data
def validate_polygons(self, polygons, ignore_tags, h, w):
"""
polygons (numpy.array, required): of shape (num_instances, num_points, 2)
"""
if len(polygons) == 0:
return polygons, ignore_tags
assert len(polygons) == len(ignore_tags)
for polygon in polygons:
polygon[:, 0] = np.clip(polygon[:, 0], 0, w - 1)
polygon[:, 1] = np.clip(polygon[:, 1], 0, h - 1)
for i in range(len(polygons)):
area = self.polygon_area(polygons[i])
if abs(area) < 1:
ignore_tags[i] = True
if area > 0:
polygons[i] = polygons[i][::-1, :]
return polygons, ignore_tags
def polygon_area(self, polygon):
return cv2.contourArea(polygon)
# edge = 0
# for i in range(polygon.shape[0]):
# next_index = (i + 1) % polygon.shape[0]
# edge += (polygon[next_index, 0] - polygon[i, 0]) * (polygon[next_index, 1] - polygon[i, 1])
#
# return edge / 2.
if __name__ == "__main__":
from shapely.geometry import Polygon
import pyclipper
polygon = np.array([[0, 0], [100, 10], [100, 100], [10, 90]])
a = shrink_polygon_py(polygon, 0.4)
print(a)
print(shrink_polygon_py(a, 1 / 0.4))
b = shrink_polygon_pyclipper(polygon, 0.4)
print(b)
poly = Polygon(b)
distance = poly.area * 1.5 / poly.length
offset = pyclipper.PyclipperOffset()
offset.AddPath(b, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON)
expanded = np.array(offset.Execute(distance))
bounding_box = cv2.minAreaRect(expanded)
points = cv2.boxPoints(bounding_box)
print(points)
@@ -0,0 +1,211 @@
import random
import cv2
import numpy as np
# random crop algorithm similar to https://github.com/argman/EAST
class EastRandomCropData:
def __init__(
self,
size=(640, 640),
max_tries=50,
min_crop_side_ratio=0.1,
require_original_image=False,
keep_ratio=True,
):
self.size = size
self.max_tries = max_tries
self.min_crop_side_ratio = min_crop_side_ratio
self.require_original_image = require_original_image
self.keep_ratio = keep_ratio
def __call__(self, data: dict) -> dict:
"""
从scales中随机选择一个尺度,对图片和文本框进行缩放
:param data: {'img':,'text_polys':,'texts':,'ignore_tags':}
:return:
"""
im = data["img"]
text_polys = data["text_polys"]
ignore_tags = data["ignore_tags"]
texts = data["texts"]
all_care_polys = [text_polys[i] for i, tag in enumerate(ignore_tags) if not tag]
# 计算crop区域
crop_x, crop_y, crop_w, crop_h = self.crop_area(im, all_care_polys)
# crop 图片 保持比例填充
scale_w = self.size[0] / crop_w
scale_h = self.size[1] / crop_h
scale = min(scale_w, scale_h)
h = int(crop_h * scale)
w = int(crop_w * scale)
if self.keep_ratio:
if len(im.shape) == 3:
padimg = np.zeros((self.size[1], self.size[0], im.shape[2]), im.dtype)
else:
padimg = np.zeros((self.size[1], self.size[0]), im.dtype)
padimg[:h, :w] = cv2.resize(
im[crop_y : crop_y + crop_h, crop_x : crop_x + crop_w], (w, h)
)
img = padimg
else:
img = cv2.resize(
im[crop_y : crop_y + crop_h, crop_x : crop_x + crop_w], tuple(self.size)
)
# crop 文本框
text_polys_crop = []
ignore_tags_crop = []
texts_crop = []
for poly, text, tag in zip(text_polys, texts, ignore_tags):
poly = ((poly - (crop_x, crop_y)) * scale).tolist()
if not self.is_poly_outside_rect(poly, 0, 0, w, h):
text_polys_crop.append(poly)
ignore_tags_crop.append(tag)
texts_crop.append(text)
data["img"] = img
data["text_polys"] = np.float32(text_polys_crop)
data["ignore_tags"] = ignore_tags_crop
data["texts"] = texts_crop
return data
def is_poly_in_rect(self, poly, x, y, w, h):
poly = np.array(poly)
if poly[:, 0].min() < x or poly[:, 0].max() > x + w:
return False
if poly[:, 1].min() < y or poly[:, 1].max() > y + h:
return False
return True
def is_poly_outside_rect(self, poly, x, y, w, h):
poly = np.array(poly)
if poly[:, 0].max() < x or poly[:, 0].min() > x + w:
return True
if poly[:, 1].max() < y or poly[:, 1].min() > y + h:
return True
return False
def split_regions(self, axis):
regions = []
min_axis = 0
for i in range(1, axis.shape[0]):
if axis[i] != axis[i - 1] + 1:
region = axis[min_axis:i]
min_axis = i
regions.append(region)
return regions
def random_select(self, axis, max_size):
xx = np.random.choice(axis, size=2)
xmin = np.min(xx)
xmax = np.max(xx)
xmin = np.clip(xmin, 0, max_size - 1)
xmax = np.clip(xmax, 0, max_size - 1)
return xmin, xmax
def region_wise_random_select(self, regions, max_size):
selected_index = list(np.random.choice(len(regions), 2))
selected_values = []
for index in selected_index:
axis = regions[index]
xx = int(np.random.choice(axis, size=1))
selected_values.append(xx)
xmin = min(selected_values)
xmax = max(selected_values)
return xmin, xmax
def crop_area(self, im, text_polys):
h, w = im.shape[:2]
h_array = np.zeros(h, dtype=np.int32)
w_array = np.zeros(w, dtype=np.int32)
for points in text_polys:
points = np.round(points, decimals=0).astype(np.int32)
minx = np.min(points[:, 0])
maxx = np.max(points[:, 0])
w_array[minx:maxx] = 1
miny = np.min(points[:, 1])
maxy = np.max(points[:, 1])
h_array[miny:maxy] = 1
# ensure the cropped area not across a text
h_axis = np.where(h_array == 0)[0]
w_axis = np.where(w_array == 0)[0]
if len(h_axis) == 0 or len(w_axis) == 0:
return 0, 0, w, h
h_regions = self.split_regions(h_axis)
w_regions = self.split_regions(w_axis)
for i in range(self.max_tries):
if len(w_regions) > 1:
xmin, xmax = self.region_wise_random_select(w_regions, w)
else:
xmin, xmax = self.random_select(w_axis, w)
if len(h_regions) > 1:
ymin, ymax = self.region_wise_random_select(h_regions, h)
else:
ymin, ymax = self.random_select(h_axis, h)
if (
xmax - xmin < self.min_crop_side_ratio * w
or ymax - ymin < self.min_crop_side_ratio * h
):
# area too small
continue
num_poly_in_rect = 0
for poly in text_polys:
if not self.is_poly_outside_rect(
poly, xmin, ymin, xmax - xmin, ymax - ymin
):
num_poly_in_rect += 1
break
if num_poly_in_rect > 0:
return xmin, ymin, xmax - xmin, ymax - ymin
return 0, 0, w, h
class PSERandomCrop:
def __init__(self, size):
self.size = size
def __call__(self, data):
imgs = data["imgs"]
h, w = imgs[0].shape[0:2]
th, tw = self.size
if w == tw and h == th:
return imgs
# label中存在文本实例,并且按照概率进行裁剪,使用threshold_label_map控制
if np.max(imgs[2]) > 0 and random.random() > 3 / 8:
# 文本实例的左上角点
tl = np.min(np.where(imgs[2] > 0), axis=1) - self.size
tl[tl < 0] = 0
# 文本实例的右下角点
br = np.max(np.where(imgs[2] > 0), axis=1) - self.size
br[br < 0] = 0
# 保证选到右下角点时,有足够的距离进行crop
br[0] = min(br[0], h - th)
br[1] = min(br[1], w - tw)
for _ in range(50000):
i = random.randint(tl[0], br[0])
j = random.randint(tl[1], br[1])
# 保证shrink_label_map有文本
if imgs[1][i : i + th, j : j + tw].sum() <= 0:
continue
else:
break
else:
i = random.randint(0, h - th)
j = random.randint(0, w - tw)
# return i, j, th, tw
for idx in range(len(imgs)):
if len(imgs[idx].shape) == 3:
imgs[idx] = imgs[idx][i : i + th, j : j + tw, :]
else:
imgs[idx] = imgs[idx][i : i + th, j : j + tw]
data["imgs"] = imgs
return data
+21
View File
@@ -0,0 +1,21 @@
name: dbnet
channels:
- conda-forge
- defaults
dependencies:
- anyconfig==0.9.10
- future==0.18.2
- imgaug==0.4.0
- matplotlib==3.1.2
- numpy==1.17.4
- opencv
- pyclipper
- PyYAML==5.2
- scikit-image==0.16.2
- Shapely==1.6.4
- tensorboard=2
- tqdm==4.40.1
- ipython
- pip
- pip:
- polygon3
+1
View File
@@ -0,0 +1 @@
CUDA_VISIBLE_DEVICES=0 python3 tools/eval.py --model_path ''
@@ -0,0 +1,17 @@
#Only use if your file names of the images and txts are identical
rm ./datasets/train_img.txt
rm ./datasets/train_gt.txt
rm ./datasets/test_img.txt
rm ./datasets/test_gt.txt
rm ./datasets/train.txt
rm ./datasets/test.txt
ls ./datasets/train/img/*.jpg > ./datasets/train_img.txt
ls ./datasets/train/gt/*.txt > ./datasets/train_gt.txt
ls ./datasets/test/img/*.jpg > ./datasets/test_img.txt
ls ./datasets/test/gt/*.txt > ./datasets/test_gt.txt
paste ./datasets/train_img.txt ./datasets/train_gt.txt > ./datasets/train.txt
paste ./datasets/test_img.txt ./datasets/test_gt.txt > ./datasets/test.txt
rm ./datasets/train_img.txt
rm ./datasets/train_gt.txt
rm ./datasets/test_img.txt
rm ./datasets/test_gt.txt
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:55
# @Author : zhoujun
import copy
from .model import Model
from .losses import build_loss
__all__ = ["build_loss", "build_model"]
support_model = ["Model"]
def build_model(config):
"""
get architecture model class
"""
copy_config = copy.deepcopy(config)
arch_type = copy_config.pop("type")
assert (
arch_type in support_model
), f"{arch_type} is not developed yet!, only {support_model} are support now"
arch_model = eval(arch_type)(copy_config)
return arch_model
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# @Time : 2019/8/23 21:54
# @Author : zhoujun
from .resnet import *
__all__ = ["build_backbone"]
support_backbone = [
"resnet18",
"deformable_resnet18",
"deformable_resnet50",
"resnet50",
"resnet34",
"resnet101",
"resnet152",
]
def build_backbone(backbone_name, **kwargs):
assert (
backbone_name in support_backbone
), f"all support backbone is {support_backbone}"
backbone = eval(backbone_name)(**kwargs)
return backbone
@@ -0,0 +1,366 @@
import math
import paddle
from paddle import nn
BatchNorm2d = nn.BatchNorm2D
__all__ = [
"ResNet",
"resnet18",
"resnet34",
"resnet50",
"resnet101",
"deformable_resnet18",
"deformable_resnet50",
"resnet152",
]
model_urls = {
"resnet18": "https://download.pytorch.org/models/resnet18-5c106cde.pth",
"resnet34": "https://download.pytorch.org/models/resnet34-333f7ec4.pth",
"resnet50": "https://download.pytorch.org/models/resnet50-19c8e357.pth",
"resnet101": "https://download.pytorch.org/models/resnet101-5d3b4d8f.pth",
"resnet152": "https://download.pytorch.org/models/resnet152-b121ed2d.pth",
}
def constant_init(module, constant, bias=0):
module.weight = paddle.create_parameter(
shape=module.weight.shape,
dtype="float32",
default_initializer=paddle.nn.initializer.Constant(constant),
)
if hasattr(module, "bias"):
module.bias = paddle.create_parameter(
shape=module.bias.shape,
dtype="float32",
default_initializer=paddle.nn.initializer.Constant(bias),
)
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2D(
in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias_attr=False
)
class BasicBlock(nn.Layer):
expansion = 1
def __init__(self, inplanes, planes, stride=1, downsample=None, dcn=None):
super(BasicBlock, self).__init__()
self.with_dcn = dcn is not None
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = BatchNorm2d(planes, momentum=0.1)
self.relu = nn.ReLU()
self.with_modulated_dcn = False
if not self.with_dcn:
self.conv2 = nn.Conv2D(
planes, planes, kernel_size=3, padding=1, bias_attr=False
)
else:
from paddle.vision.ops import DeformConv2D
deformable_groups = dcn.get("deformable_groups", 1)
offset_channels = 18
self.conv2_offset = nn.Conv2D(
planes, deformable_groups * offset_channels, kernel_size=3, padding=1
)
self.conv2 = DeformConv2D(
planes, planes, kernel_size=3, padding=1, bias_attr=False
)
self.bn2 = BatchNorm2d(planes, momentum=0.1)
self.downsample = downsample
self.stride = stride
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# out = self.conv2(out)
if not self.with_dcn:
out = self.conv2(out)
else:
offset = self.conv2_offset(out)
out = self.conv2(out, offset)
out = self.bn2(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class Bottleneck(nn.Layer):
expansion = 4
def __init__(self, inplanes, planes, stride=1, downsample=None, dcn=None):
super(Bottleneck, self).__init__()
self.with_dcn = dcn is not None
self.conv1 = nn.Conv2D(inplanes, planes, kernel_size=1, bias_attr=False)
self.bn1 = BatchNorm2d(planes, momentum=0.1)
self.with_modulated_dcn = False
if not self.with_dcn:
self.conv2 = nn.Conv2D(
planes, planes, kernel_size=3, stride=stride, padding=1, bias_attr=False
)
else:
deformable_groups = dcn.get("deformable_groups", 1)
from paddle.vision.ops import DeformConv2D
offset_channels = 18
self.conv2_offset = nn.Conv2D(
planes,
deformable_groups * offset_channels,
stride=stride,
kernel_size=3,
padding=1,
)
self.conv2 = DeformConv2D(
planes, planes, kernel_size=3, padding=1, stride=stride, bias_attr=False
)
self.bn2 = BatchNorm2d(planes, momentum=0.1)
self.conv3 = nn.Conv2D(planes, planes * 4, kernel_size=1, bias_attr=False)
self.bn3 = BatchNorm2d(planes * 4, momentum=0.1)
self.relu = nn.ReLU()
self.downsample = downsample
self.stride = stride
self.dcn = dcn
self.with_dcn = dcn is not None
def forward(self, x):
residual = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
# out = self.conv2(out)
if not self.with_dcn:
out = self.conv2(out)
else:
offset = self.conv2_offset(out)
out = self.conv2(out, offset)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Layer):
def __init__(self, block, layers, in_channels=3, dcn=None):
self.dcn = dcn
self.inplanes = 64
super(ResNet, self).__init__()
self.out_channels = []
self.conv1 = nn.Conv2D(
in_channels, 64, kernel_size=7, stride=2, padding=3, bias_attr=False
)
self.bn1 = BatchNorm2d(64, momentum=0.1)
self.relu = nn.ReLU()
self.maxpool = nn.MaxPool2D(kernel_size=3, stride=2, padding=1)
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2, dcn=dcn)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2, dcn=dcn)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2, dcn=dcn)
if self.dcn is not None:
for m in self.modules():
if isinstance(m, Bottleneck) or isinstance(m, BasicBlock):
if hasattr(m, "conv2_offset"):
constant_init(m.conv2_offset, 0)
def _make_layer(self, block, planes, blocks, stride=1, dcn=None):
downsample = None
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2D(
self.inplanes,
planes * block.expansion,
kernel_size=1,
stride=stride,
bias_attr=False,
),
BatchNorm2d(planes * block.expansion, momentum=0.1),
)
layers = []
layers.append(block(self.inplanes, planes, stride, downsample, dcn=dcn))
self.inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(block(self.inplanes, planes, dcn=dcn))
self.out_channels.append(planes * block.expansion)
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x2 = self.layer1(x)
x3 = self.layer2(x2)
x4 = self.layer3(x3)
x5 = self.layer4(x4)
return x2, x3, x4, x5
def load_torch_params(paddle_model, torch_patams):
paddle_params = paddle_model.state_dict()
fc_names = ["classifier"]
for key, torch_value in torch_patams.items():
if "num_batches_tracked" in key:
continue
key = (
key.replace("running_var", "_variance")
.replace("running_mean", "_mean")
.replace("module.", "")
)
torch_value = torch_value.detach().cpu().numpy()
if key in paddle_params:
flag = [i in key for i in fc_names]
if any(flag) and "weight" in key: # ignore bias
new_shape = [1, 0] + list(range(2, torch_value.ndim))
print(
f"name: {key}, ori shape: {torch_value.shape}, new shape: {torch_value.transpose(new_shape).shape}"
)
torch_value = torch_value.transpose(new_shape)
paddle_params[key] = torch_value
else:
print(f"{key} not in paddle")
paddle_model.set_state_dict(paddle_params)
def load_models(model, model_name):
import torch.utils.model_zoo as model_zoo
torch_patams = model_zoo.load_url(model_urls[model_name])
load_torch_params(model, torch_patams)
def resnet18(pretrained=True, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
print("load from imagenet")
load_models(model, "resnet18")
return model
def deformable_resnet18(pretrained=True, **kwargs):
"""Constructs a ResNet-18 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [2, 2, 2, 2], dcn=dict(deformable_groups=1), **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
print("load from imagenet")
model.load_state_dict(model_zoo.load_url(model_urls["resnet18"]), strict=False)
return model
def resnet34(pretrained=True, **kwargs):
"""Constructs a ResNet-34 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
model.load_state_dict(model_zoo.load_url(model_urls["resnet34"]), strict=False)
return model
def resnet50(pretrained=True, **kwargs):
"""Constructs a ResNet-50 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
load_models(model, "resnet50")
return model
def deformable_resnet50(pretrained=True, **kwargs):
"""Constructs a ResNet-50 model with deformable conv.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 6, 3], dcn=dict(deformable_groups=1), **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
model.load_state_dict(model_zoo.load_url(model_urls["resnet50"]), strict=False)
return model
def resnet101(pretrained=True, **kwargs):
"""Constructs a ResNet-101 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
model.load_state_dict(model_zoo.load_url(model_urls["resnet101"]), strict=False)
return model
def resnet152(pretrained=True, **kwargs):
"""Constructs a ResNet-152 model.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
"""
model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs)
if pretrained:
assert (
kwargs.get("in_channels", 3) == 3
), "in_channels must be 3 when pretrained is True"
model.load_state_dict(model_zoo.load_url(model_urls["resnet152"]), strict=False)
return model
if __name__ == "__main__":
x = paddle.zeros([2, 3, 640, 640])
net = resnet50(pretrained=True)
y = net(x)
for u in y:
print(u.shape)
print(net.out_channels)
+40
View File
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/6 11:19
# @Author : zhoujun
from paddle import nn
class ConvBnRelu(nn.Layer):
def __init__(
self,
in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode="zeros",
inplace=True,
):
super().__init__()
self.conv = nn.Conv2D(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
bias_attr=bias,
padding_mode=padding_mode,
)
self.bn = nn.BatchNorm2D(out_channels)
self.relu = nn.ReLU()
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# @Time : 2019/12/4 14:54
# @Author : zhoujun
import paddle
from paddle import nn, ParamAttr
class DBHead(nn.Layer):
def __init__(self, in_channels, out_channels, k=50):
super().__init__()
self.k = k
self.binarize = nn.Sequential(
nn.Conv2D(
in_channels,
in_channels // 4,
3,
padding=1,
weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()),
),
nn.BatchNorm2D(
in_channels // 4,
weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)),
bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4)),
),
nn.ReLU(),
nn.Conv2DTranspose(
in_channels // 4,
in_channels // 4,
2,
2,
weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()),
),
nn.BatchNorm2D(
in_channels // 4,
weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)),
bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4)),
),
nn.ReLU(),
nn.Conv2DTranspose(
in_channels // 4, 1, 2, 2, weight_attr=nn.initializer.KaimingNormal()
),
nn.Sigmoid(),
)
self.thresh = self._init_thresh(in_channels)
def forward(self, x):
shrink_maps = self.binarize(x)
threshold_maps = self.thresh(x)
if self.training:
binary_maps = self.step_function(shrink_maps, threshold_maps)
y = paddle.concat((shrink_maps, threshold_maps, binary_maps), axis=1)
else:
y = paddle.concat((shrink_maps, threshold_maps), axis=1)
return y
def _init_thresh(self, inner_channels, serial=False, smooth=False, bias=False):
in_channels = inner_channels
if serial:
in_channels += 1
self.thresh = nn.Sequential(
nn.Conv2D(
in_channels,
inner_channels // 4,
3,
padding=1,
bias_attr=bias,
weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()),
),
nn.BatchNorm2D(
inner_channels // 4,
weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)),
bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4)),
),
nn.ReLU(),
self._init_upsample(
inner_channels // 4, inner_channels // 4, smooth=smooth, bias=bias
),
nn.BatchNorm2D(
inner_channels // 4,
weight_attr=ParamAttr(initializer=nn.initializer.Constant(1)),
bias_attr=ParamAttr(initializer=nn.initializer.Constant(1e-4)),
),
nn.ReLU(),
self._init_upsample(inner_channels // 4, 1, smooth=smooth, bias=bias),
nn.Sigmoid(),
)
return self.thresh
def _init_upsample(self, in_channels, out_channels, smooth=False, bias=False):
if smooth:
inter_out_channels = out_channels
if out_channels == 1:
inter_out_channels = in_channels
module_list = [
nn.Upsample(scale_factor=2, mode="nearest"),
nn.Conv2D(
in_channels,
inter_out_channels,
3,
1,
1,
bias_attr=bias,
weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()),
),
]
if out_channels == 1:
module_list.append(
nn.Conv2D(
in_channels,
out_channels,
kernel_size=1,
stride=1,
padding=1,
bias_attr=True,
weight_attr=ParamAttr(
initializer=nn.initializer.KaimingNormal()
),
)
)
return nn.Sequential(module_list)
else:
return nn.Conv2DTranspose(
in_channels,
out_channels,
2,
2,
weight_attr=ParamAttr(initializer=nn.initializer.KaimingNormal()),
)
def step_function(self, x, y):
return paddle.reciprocal(1 + paddle.exp(-self.k * (x - y)))
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# @Time : 2020/6/5 11:35
# @Author : zhoujun
from .DBHead import DBHead
__all__ = ["build_head"]
support_head = ["DBHead"]
def build_head(head_name, **kwargs):
assert head_name in support_head, f"all support head is {support_head}"
head = eval(head_name)(**kwargs)
return head

Some files were not shown because too many files have changed in this diff Show More