├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── exploration-notebooks └── .gitkeep ├── img ├── 0.png └── unstructured_logo.png ├── lib └── libstdc++.so.6 ├── logger_config.yaml ├── pipeline-notebooks ├── .gitkeep └── pipeline-paddleocr.ipynb ├── prepline_paddleocr ├── __init__.py └── api │ ├── __init__.py │ ├── app.py │ └── paddleocr.py ├── preprocessing-pipeline-family.yaml ├── requirements ├── base.in ├── base.txt ├── dev.in ├── dev.txt ├── test.in └── test.txt ├── sample-docs ├── .gitkeep └── sample-receipt.jpg ├── scripts ├── check-and-format-notebooks.py ├── docker-build.sh ├── shellcheck.sh ├── test-doc-pipeline-apis-consistent.sh └── version-sync.sh ├── setup.cfg └── test_paddleocr └── api ├── .gitkeep └── test_paddleocr.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/requirements" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | # NOTE(robinson) - Workflow files stored in the 10 | # default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | PYTHON_VERSION: "3.8" 11 | PIPELINE_FAMILY: "paddleocr" 12 | 13 | jobs: 14 | setup: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/cache@v3 19 | id: virtualenv-cache 20 | with: 21 | path: | 22 | .venv 23 | key: ci-venv-${{ env.PIPELINE_FAMILY }}-${{ hashFiles('requirements/*.txt') }} 24 | - name: Set up Python ${{ env.PYTHON_VERSION }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ env.PYTHON_VERSION }} 28 | - name: Setup virtual environment (no cache hit) 29 | if: steps.virtualenv-cache.outputs.cache-hit != 'true' 30 | run: | 31 | python${{ env.PYTHON_VERSION }} -m venv .venv 32 | source .venv/bin/activate 33 | make install 34 | 35 | lint: 36 | runs-on: ubuntu-latest 37 | needs: setup 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: actions/cache@v3 41 | id: virtualenv-cache 42 | with: 43 | path: | 44 | .venv 45 | key: ci-venv-${{ env.PIPELINE_FAMILY }}-${{ hashFiles('requirements/*.txt') }} 46 | - name: Lint 47 | run: | 48 | source .venv/bin/activate 49 | make check 50 | 51 | shellcheck: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: ShellCheck 56 | uses: ludeeus/action-shellcheck@master 57 | 58 | test: 59 | runs-on: ubuntu-latest 60 | needs: [setup, lint] 61 | steps: 62 | - uses: actions/checkout@v3 63 | - uses: actions/cache@v3 64 | id: virtualenv-cache 65 | with: 66 | path: | 67 | .venv 68 | key: ci-venv-${{ env.PIPELINE_FAMILY }}-${{ hashFiles('requirements/*.txt') }} 69 | - name: Run core tests 70 | run: | 71 | source .venv/bin/activate 72 | sudo apt-get install --yes poppler-utils 73 | make test 74 | make check-coverage 75 | make check-notebooks 76 | 77 | changelog: 78 | runs-on: ubuntu-latest 79 | steps: 80 | - uses: actions/checkout@v3 81 | - if: github.ref != 'refs/heads/main' 82 | uses: dorny/paths-filter@v2 83 | id: changes 84 | with: 85 | filters: | 86 | src: 87 | - 'doc_recipe/**' 88 | - 'recipe-notebooks/**' 89 | 90 | - if: steps.changes.outputs.src == 'true' && github.ref != 'refs/heads/main' 91 | uses: dangoslen/changelog-enforcer@v3 92 | 93 | api_consistency: 94 | runs-on: ubuntu-latest 95 | needs: setup 96 | steps: 97 | - uses: actions/checkout@v3 98 | - uses: actions/cache@v3 99 | id: virtualenv-cache 100 | with: 101 | path: | 102 | .venv 103 | key: ci-venv-${{ env.PIPELINE_FAMILY }}-${{ hashFiles('requirements/*.txt') }} 104 | - name: API Consistency 105 | run: | 106 | source .venv/bin/activate 107 | make api-check 108 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '21 21 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VSCode 132 | .vscode/ 133 | 134 | # Mac 135 | .DS_Store 136 | 137 | nbs/ 138 | 139 | # Celery files that are created when the mercury dashboard is run 140 | celery.sqlite 141 | celerybeat-schedule.db 142 | 143 | # temporarily generated files by project-specific Makefile 144 | tmp* 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:experimental 2 | 3 | FROM centos:centos7.9.2009 4 | 5 | # NOTE(crag): NB_USER ARG for mybinder.org compat: 6 | # https://mybinder.readthedocs.io/en/latest/tutorials/dockerfile.html 7 | ARG NB_USER=notebook-user 8 | ARG NB_UID=1000 9 | ARG PIP_VERSION 10 | ARG PIPELINE_PACKAGE 11 | 12 | ENV DEBIAN_FRONTEND=noninteractive 13 | 14 | RUN yum update -y 15 | RUN yum upgrade -y 16 | 17 | RUN yum install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \ 18 | libreadline-dev libsqlite3-dev wget curl libncurses5-dev libncursesw5-dev \ 19 | xz-utils tk-dev libffi-dev liblzma-dev git mesa-libGL 20 | 21 | RUN yum -y install gcc openssl-devel bzip2-devel libffi-devel make git sqlite-devel && \ 22 | curl -O https://www.python.org/ftp/python/3.8.15/Python-3.8.15.tgz && tar -xzf Python-3.8.15.tgz && \ 23 | cd Python-3.8.15/ && ./configure --enable-shared --enable-optimizations && make altinstall && \ 24 | cd .. && rm -rf Python-3.8.15* && \ 25 | ln -s /usr/local/bin/python3.8 /usr/local/bin/python3 26 | 27 | COPY lib/libstdc++.so.6 /usr/lib64 28 | 29 | # create user with a home directory 30 | ENV USER ${NB_USER} 31 | ENV HOME /home/${NB_USER} 32 | 33 | RUN groupadd --gid ${NB_UID} ${NB_USER} 34 | RUN useradd --uid ${NB_UID} --gid ${NB_UID} ${NB_USER} 35 | USER ${NB_USER} 36 | WORKDIR ${HOME} 37 | ENV PYTHONPATH="${PYTHONPATH}:${HOME}" 38 | ENV PATH="/home/${NB_USER}/.local/bin:${PATH}" 39 | ENV LD_LIBRARY_PATH=/usr/local/lib 40 | ENV LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH 41 | 42 | COPY logger_config.yaml logger_config.yaml 43 | COPY requirements/dev.txt requirements-dev.txt 44 | COPY requirements/base.txt requirements-base.txt 45 | COPY prepline_${PIPELINE_PACKAGE}/ prepline_${PIPELINE_PACKAGE}/ 46 | COPY exploration-notebooks exploration-notebooks 47 | COPY pipeline-notebooks pipeline-notebooks 48 | COPY img/ img/ 49 | 50 | #RUN echo 'export LD_LIBRARY_PATH=/usr/local/lib' >> ~/.bashrc 51 | 52 | RUN python3.8 -m pip install pip==${PIP_VERSION} \ 53 | && pip3.8 install --no-cache -r requirements-base.txt \ 54 | && pip3.8 install --no-cache -r requirements-dev.txt 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PIPELINE_FAMILY := paddleocr 2 | PIPELINE_PACKAGE := paddleocr 3 | PACKAGE_NAME := prepline_${PIPELINE_PACKAGE} 4 | PIP_VERSION := 23.1.2 5 | 6 | .PHONY: help 7 | help: Makefile 8 | @sed -n 's/^\(## \)\([a-zA-Z]\)/\2/p' $< 9 | 10 | 11 | ########### 12 | # Install # 13 | ########### 14 | 15 | ## install-base: installs minimum requirements to run the API 16 | .PHONY: install-base 17 | install-base: install-base-pip-packages 18 | 19 | ## install: installs all test and dev requirements 20 | .PHONY: install 21 | install: install-base install-test install-dev 22 | 23 | .PHONY: install-base-pip-packages 24 | install-base-pip-packages: 25 | python3 -m pip install pip==${PIP_VERSION} 26 | pip install -r requirements/base.txt 27 | 28 | .PHONY: install-test 29 | install-test: 30 | pip install -r requirements/test.txt 31 | 32 | .PHONY: install-dev 33 | install-dev: 34 | pip install -r requirements/dev.txt 35 | 36 | .PHONY: install-ci 37 | install-ci: install-base install-test 38 | 39 | ## pip-compile: compiles all base/dev/test requirements 40 | .PHONY: pip-compile 41 | pip-compile: 42 | pip-compile requirements/base.in 43 | pip-compile requirements/dev.in 44 | pip-compile requirements/test.in 45 | 46 | 47 | ######### 48 | # Build # 49 | ######### 50 | 51 | ## generate-api: generates the FastAPI python APIs from notebooks 52 | .PHONY: generate-api 53 | generate-api: 54 | PYTHONPATH=. unstructured_api_tools convert-pipeline-notebooks \ 55 | --input-directory ./pipeline-notebooks \ 56 | --output-directory ./${PACKAGE_NAME}/api 57 | 58 | ########## 59 | # Docker # 60 | ########## 61 | 62 | # Docker targets are provided for convenience only and are not required in a standard development environment 63 | 64 | # Note that the image has notebooks baked in, however the current working directory 65 | # is mounted under /home/notebook-user/local/ when the image is started with 66 | # docker-start-api or docker-start-jupyter 67 | 68 | .PHONY: docker-build 69 | docker-build: 70 | PIP_VERSION=${PIP_VERSION} PIPELINE_FAMILY=${PIPELINE_FAMILY} PIPELINE_PACKAGE=${PIPELINE_PACKAGE} ./scripts/docker-build.sh 71 | 72 | .PHONY: docker-start-api 73 | docker-start-api: 74 | docker run -p 8000:8000 --mount type=bind,source=$(realpath .),target=/home/notebook-user/local -t --rm pipeline-family-${PIPELINE_FAMILY}-dev:latest uvicorn ${PACKAGE_NAME}.api.app:app --log-config logger_config.yaml --host 0.0.0.0 --port 8000 75 | 76 | .PHONY: docker-start-jupyter 77 | docker-start-jupyter: 78 | docker run -p 8888:8888 --mount type=bind,source=$(realpath .),target=/home/notebook-user/local -t --rm pipeline-family-${PIPELINE_FAMILY}-dev:latest jupyter-notebook --port 8888 --ip 0.0.0.0 --no-browser --NotebookApp.token='' --NotebookApp.password='' 79 | 80 | 81 | ######### 82 | # Local # 83 | ######### 84 | 85 | ## run-jupyter: starts jupyter notebook 86 | .PHONY: run-jupyter 87 | run-jupyter: 88 | PYTHONPATH=$(realpath .) JUPYTER_PATH=$(realpath .) jupyter-notebook --NotebookApp.token='' --NotebookApp.password='' 89 | 90 | ## run-web-app: runs the FastAPI api with hot reloading 91 | .PHONY: run-web-app 92 | run-web-app: 93 | PYTHONPATH=$(realpath .) uvicorn ${PACKAGE_NAME}.api.app:app --log-config logger_config.yaml --reload 94 | 95 | 96 | ################# 97 | # Test and Lint # 98 | ################# 99 | 100 | ## test: runs core tests 101 | .PHONY: test 102 | test: 103 | PYTHONPATH=. pytest test_${PIPELINE_PACKAGE} --cov=${PACKAGE_NAME} --cov-report term-missing 104 | 105 | .PHONY: check-coverage 106 | check-coverage: 107 | coverage report --fail-under=90 108 | 109 | ## test-integration: runs integration tests 110 | .PHONY: test-integration 111 | test-integration: 112 | PYTHONPATH=. pytest test_${PIPELINE_PACKAGE}_integration 113 | 114 | ## api-check: verifies auto-generated pipeline APIs match the existing ones 115 | .PHONY: api-check 116 | api-check: 117 | PYTHONPATH=. PACKAGE_NAME=${PACKAGE_NAME} ./scripts/test-doc-pipeline-apis-consistent.sh 118 | 119 | ## check: runs linters (includes tests) 120 | .PHONY: check 121 | check: check-src check-tests check-version 122 | 123 | ## check-src: runs linters (source only, no tests) 124 | .PHONY: check-src 125 | check-src: 126 | black --line-length 100 ${PACKAGE_NAME} --check --exclude ${PACKAGE_NAME}/api 127 | flake8 ${PACKAGE_NAME} 128 | mypy ${PACKAGE_NAME} --ignore-missing-imports --install-types --non-interactive --implicit-optional 129 | 130 | .PHONY: check-tests 131 | check-tests: 132 | black --line-length 100 test_${PIPELINE_PACKAGE} --check 133 | flake8 test_${PIPELINE_PACKAGE} 134 | 135 | ## tidy: run black 136 | .PHONY: tidy 137 | tidy: 138 | black --line-length 100 ${PACKAGE_NAME} 139 | black --line-length 100 test_${PIPELINE_PACKAGE} 140 | 141 | ## check-scripts: run shellcheck 142 | .PHONY: check-scripts 143 | check-scripts: 144 | # Fail if any of these files have warnings 145 | scripts/shellcheck.sh 146 | 147 | ## check-version: run check to ensure version in CHANGELOG.md matches references in files 148 | .PHONY: check-version 149 | check-version: 150 | # Fail if syncing version would produce changes 151 | scripts/version-sync.sh -c \ 152 | -s CHANGELOG.md \ 153 | -f README.md api-release \ 154 | -f preprocessing-pipeline-family.yaml release 155 | 156 | ## check-notebooks: check that executing and cleaning notebooks doesn't produce changes 157 | .PHONY: check-notebooks 158 | check-notebooks: 159 | scripts/check-and-format-notebooks.py --check 160 | 161 | ## tidy-notebooks: execute notebooks and remove metadata 162 | .PHONY: tidy-notebooks 163 | tidy-notebooks: 164 | scripts/check-and-format-notebooks.py 165 | 166 | ## version-sync: update references to version with most recent version from CHANGELOG.md 167 | .PHONY: version-sync 168 | version-sync: 169 | scripts/version-sync.sh \ 170 | -s CHANGELOG.md \ 171 | -f README.md api-release \ 172 | -f preprocessing-pipeline-family.yaml release 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 |

Pre-Processing OCR Pipeline for PaddleOCR

7 |

8 | 9 |
10 | 11 | ![https://pypi.python.org/pypi/unstructured/](https://img.shields.io/pypi/l/unstructured.svg) 12 | ![https://pypi.python.org/pypi/unstructured/](https://img.shields.io/pypi/pyversions/unstructured.svg) 13 | ![https://GitHub.com/unstructured-io/unstructured.js/graphs/contributors](https://img.shields.io/github/contributors/unstructured-io/unstructured) 14 | ![code_of_conduct.md](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg) 15 | ![https://github.com/Naereen/badges/](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github) 16 | 17 |
18 | 19 | 20 | This pipeline processes input image documents in the English language using [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR). 21 | The pipeline works on `x86_64` cpus. 22 | 23 | ## Developer Quick Start 24 | 25 | * Using `pyenv` to manage virtualenvs is recommended 26 | * Mac install instructions. See [here](https://github.com/Unstructured-IO/community#mac--homebrew) for more detailed instructions. 27 | * `brew install pyenv-virtualenv` 28 | * `pyenv install 3.8.15` 29 | * Linux instructions are available [here](https://github.com/Unstructured-IO/community#linux). 30 | 31 | * Create a virtualenv to work in and activate it, e.g. for one named `paddleocr`: 32 | 33 | `pyenv virtualenv 3.8.15 paddleocr`
34 | `pyenv activate paddleocr` 35 | 36 | * If you are on a Mac with an M1 chip, run `brew install mupdf swig freetype` to install 37 | required non-Python dependencies. 38 | * Run `make install` 39 | * Start a local jupyter notebook server with `make run-jupyter`
40 | **OR**
41 | just start the fast-API locally with `make run-web-app` 42 | 43 | ### Performing OCR on a JPG image 44 | 45 | To run OCR on a JPG image, run `make run-web-app` and run the following `curl` command, 46 | replacing `sample-docs/sample-receipt.jpg` with your filename: 47 | 48 | ``` 49 | curl -X 'POST' \ 50 | 'http://localhost:8000/paddleocr/v0.0.1/paddleocr' \ 51 | -H 'accept: application/json' \ 52 | -H 'Content-Type: multipart/form-data' \ 53 | -F 'files=@sample-docs/sample-receipt.jpg' | jq -C . | less -R 54 | ``` 55 | 56 | The result should look like the following. 57 | 58 | ``` 59 | "{\"result\": [[[[162.0, 111.0], [429.0, 110.0], [429.0, 138.0], [162.0, 139.0]], [\"PETRON BKT 60 | LANJAN SB\", 0.918]], [[[162.0, 142.0], [418.0, 141.0], [418.0, 170.0], [162.0, 171.0]], [\"ALSERKAM 61 | ENTERPRISE\", 0.9785]], [[[44.0, 178.0], [562.0, 175.0], [562.0, 199.0], [44.0, 202.0]], [\"Te1 62 | 03-6156 8757 Co No 001083069-M\", 0.9282]], [[[121.0, 209.0], [467.0, 209.0], [467.0, 232.0], 63 | [121.0, 232.0]], [\"KM 458.4 BKT LANJAN UTARA,\", 0.9205]], [[[95.0, 239.0], [484.0, 237.0], [484.0, 64 | 264.0], [95.0, 267.0]], [\"L/RAYA UTARA SELATAN,SG BULOH\", 0.9525]], [[[188.0, 270.0], [403.0, 65 | 270.0], [403.0, 298.0], [188.0, 298.0]], [\"47000 SUNGAI BUL\", 0.9704]], [[[139.0, 335.0], [443.0, 66 | 335.0], [443.0, 359.0], [139.0, 359.0]], [\"GST ID No001210736640\", 0.9619]], [[[217.0, 397.0], 67 | [366.0, 397.0], [366.0, 424.0], [217.0, 424.0]], [\"TAX INVOICE\", 0.9886]], [[[29.0, 491.0], 68 | [351.0, 490.0], [351.0, 518.0], [29.0, 519.0]], [\"TAX INVOICE NO 19729058\", 0.963]], [[[28.0, 69 | 523.0], [129.0, 523.0], [129.0, 552.0], [28.0, 552.0]], [\"POS1\", 0.9617]], [[[29.0, 554.0], 70 | [272.0, 552.0], [272.0, 582.0], [29.0, 583.0]], [\"Store No.:129077\", 0.9439]], [[[492.0, 552.0], 71 | [553.0, 552.0], [553.0, 584.0], [492.0, 584.0]], [\"Babu\", 0.9968]], [[[28.0, 586.0], [169.0, 72 | 589.0], [169.0, 618.0], [27.0, 615.0]], [\"01/02/2018\", 0.9972]], [[[162.0, 587.0], [340.0, 587.0], 73 | [340.0, 615.0], [162.0, 615.0]], [\"4:43:17PM\", 0.8981]], [[[28.0, 683.0], [311.0, 683.0], [311.0, 74 | 711.0], [28.0, 711.0]], [\"A 2 doublemint te\", 0.9652]], [[[506.0, 679.0], [566.0, 679.0], [566.0, 75 | 710.0], [506.0, 710.0]], [\"3.00\", 0.9931]], [[[25.0, 714.0], [313.0, 712.0], [314.0, 742.0], 76 | [25.0, 743.0]], [\"A1sandwich vanill\", 0.9318]], [[[507.0, 711.0], [566.0, 711.0], [566.0, 743.0], 77 | [507.0, 743.0]], [\"1.90\", 0.9937]], [[[69.0, 778.0], [165.0, 778.0], [165.0, 807.0], [69.0, 78 | 807.0]], [\"GST RM\", 0.9119]], [[[505.0, 775.0], [566.0, 775.0], [566.0, 807.0], [505.0, 807.0]], 79 | [\"0.28\", 0.9929]], [[[70.0, 811.0], [296.0, 811.0], [296.0, 839.0], [70.0, 839.0]], [\"Total RM 80 | inc.GST:\", 0.9176]], [[[506.0, 807.0], [566.0, 807.0], [566.0, 839.0], [506.0, 839.0]], [\"4.90\", 81 | 0.9949]], [[[67.0, 873.0], [128.0, 873.0], [128.0, 905.0], [67.0, 905.0]], [\"Cash\", 0.9938]], 82 | [[[505.0, 868.0], [568.0, 868.0], [568.0, 905.0], [505.0, 905.0]], [\"5.00\", 0.992]], [[[67.0, 83 | 904.0], [154.0, 908.0], [153.0, 938.0], [66.0, 935.0]], [\"Change\", 0.9971]], [[[506.0, 903.0], 84 | [566.0, 903.0], [566.0, 935.0], [506.0, 935.0]], [\"0.10\", 0.9981]], [[[29.0, 968.0], [179.0, 85 | 973.0], [178.0, 1002.0], [29.0, 998.0]], [\"GsT Summary\", 0.8839]], [[[242.0, 969.0], [387.0, 86 | 966.0], [388.0, 996.0], [242.0, 999.0]], [\"AnountRM\", 0.895]], [[[454.0, 969.0], [562.0, 969.0], 87 | [562.0, 998.0], [454.0, 998.0]], [\"Tax (RM)\", 0.8915]], [[[29.0, 1002.0], [128.0, 1002.0], [128.0, 88 | 1033.0], [29.0, 1033.0]], [\"A=6.00%\", 0.9756]], [[[241.0, 1001.0], [301.0, 1001.0], [301.0, 89 | 1033.0], [241.0, 1033.0]], [\"4.62\", 0.9949]], [[[452.0, 999.0], [513.0, 999.0], [513.0, 1031.0], 90 | [452.0, 1031.0]], [\"0.28\", 0.9955]], [[[29.0, 1070.0], [47.0, 1070.0], [47.0, 1092.0], [29.0, 91 | 1092.0]], [\"A\", 0.9864]], [[[106.0, 1066.0], [418.0, 1066.0], [418.0, 1094.0], [106.0, 1094.0]], 92 | [\"ITAL INCLUDES 6.00%GST\", 0.9485]], [[[151.0, 1166.0], [429.0, 1166.0], [429.0, 1190.0], [151.0, 93 | 1190.0]], [\"Use 3000 Petron Miles\", 0.9395]], [[[176.0, 1197.0], [403.0, 1194.0], [403.0, 1223.0], 94 | [176.0, 1226.0]], [\"points to pay for\", 0.9474]], [[[228.0, 1227.0], [351.0, 1227.0], [351.0, 95 | 1257.0], [228.0, 1257.0]], [\"RM45 Fue1\", 0.932]]]} 96 | ``` 97 | 98 | You can also run OCR through the Python API using the following commands: 99 | 100 | ```python 101 | from prepline_paddleocr.api.paddleocr import pipeline_api 102 | 103 | filename = "sample-docs/sample-receipt.jpg" 104 | 105 | with open(filename, "rb") as f: 106 | pipeline_api(file=f) 107 | ``` 108 | 109 | 110 | ### Generating Python files from the pipeline notebooks 111 | 112 | You can generate the FastAPI APIs from your pipeline notebooks by running `make generate-api`. 113 | 114 | ## Security Policy 115 | 116 | See our [security policy](https://github.com/Unstructured-IO/pipeline-paddleocr/security/policy) for 117 | information on how to report security vulnerabilities. 118 | 119 | ## Learn more 120 | 121 | | Section | Description | 122 | |-|-| 123 | | [Unstructured Community Github](https://github.com/Unstructured-IO/community) | Information about Unstructured.io community projects | 124 | | [Unstructured Github](https://github.com/Unstructured-IO) | Unstructured.io open source repositories | 125 | | [Company Website](https://unstructured.io) | Unstructured.io product and company info | 126 | -------------------------------------------------------------------------------- /exploration-notebooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/exploration-notebooks/.gitkeep -------------------------------------------------------------------------------- /img/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/img/0.png -------------------------------------------------------------------------------- /img/unstructured_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/img/unstructured_logo.png -------------------------------------------------------------------------------- /lib/libstdc++.so.6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/lib/libstdc++.so.6 -------------------------------------------------------------------------------- /logger_config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | formatters: 4 | default_format: 5 | "()": uvicorn.logging.DefaultFormatter 6 | format: '%(asctime)s %(name)s %(levelname)s %(message)s' 7 | access: 8 | "()": uvicorn.logging.AccessFormatter 9 | format: '%(asctime)s %(client_addr)s %(request_line)s - %(status_code)s' 10 | handlers: 11 | access_handler: 12 | formatter: access 13 | class: logging.StreamHandler 14 | stream: ext://sys.stderr 15 | standard_handler: 16 | formatter: default_format 17 | class: logging.StreamHandler 18 | stream: ext://sys.stderr 19 | loggers: 20 | uvicorn.error: 21 | level: INFO 22 | handlers: 23 | - standard_handler 24 | propagate: no 25 | # disable logging for uvicorn.error by not having a handler 26 | uvicorn.access: 27 | level: INFO 28 | handlers: 29 | - access_handler 30 | propagate: no 31 | # disable logging for uvicorn.access by not having a handler 32 | unstructured: 33 | level: INFO 34 | handlers: 35 | - standard_handler 36 | propagate: no 37 | 38 | -------------------------------------------------------------------------------- /pipeline-notebooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/pipeline-notebooks/.gitkeep -------------------------------------------------------------------------------- /pipeline-notebooks/pipeline-paddleocr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "3931743a", 6 | "metadata": {}, 7 | "source": [ 8 | "# PaddleOCR Pipeline" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "757bd7cd", 14 | "metadata": {}, 15 | "source": [ 16 | "## Section 1: Introduction\n", 17 | "\n", 18 | "The goal of this notebook is to setup a pipeline for PaddleOCR" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "id": "7db1e471", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "import json\n", 29 | "import os\n", 30 | "\n", 31 | "def get_filename(directory, filename):\n", 32 | " cwd = os.getcwd()\n", 33 | " local_directory = os.path.join(os.path.split(cwd)[0], directory)\n", 34 | " ci_directory = os.path.join(cwd, directory)\n", 35 | "\n", 36 | " if os.path.exists(local_directory) and filename in os.listdir(local_directory):\n", 37 | " return os.path.join(local_directory, filename)\n", 38 | " elif os.path.exists(ci_directory) and filename in os.listdir(ci_directory):\n", 39 | " return os.path.join(ci_directory, filename)\n", 40 | " else:\n", 41 | " raise FileNotFoundError" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "id": "48daac01", 47 | "metadata": {}, 48 | "source": [ 49 | "## Show example image" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "id": "18bbc559", 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "data": { 60 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmQAAAMYCAIAAADq5GzlAABtcUlEQVR4nO3deXxM1+P/8ZPJHtn3jSzEkir92NpKi6o1pVqKqp3wse/U0uKDoqit6mOtWkspVXuptRX7HiKSiOxk35NJZub3x3185pdvQo62yGhfzz88Zu6ce+bcmzHvueeee66RTqcTAADgyVSV3QAAAAwdYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCABGEJAIAEYQkAgARhCQCAhEllN+DxiouLMzIybGxsLC0thRBqtTozM9PJycnY2Fir1aamplpYWNja2gohlKel17W0tLSxsamcdgMA/o4M9Mhy165dwcHBixYtUp5eu3YtODj4wYMHQohFixZ169YtOTlZeSk1NTX4//rvf//7Ipt65cqVlStXvsh3BAC8YAZ6ZHno0CEbG5vjx49PnjzZ1NRUv3zDhg179+795ptvatasqSxxdnY+ePCgEOKDDz7o2rVrz549LS0tr1275urqmpqaWqtWLRMTkytXrhQUFLz66qsODg7R0dHFxcXW1tb3799v0KCBlZWVEOLevXtJSUk1atTw9PTMz8+/detWjRo1wsPDfX19PT09hRAFBQU3b940MTGpV6+eiYlJSkpKTExMrVq14uPjQ0NDd+3a9dFHH7m6ulbGrgLw0rh9+7YQwt7eXvliEUIUFxffu3dPCFGlShUfH58K1o2JicnPz7exsalataoQIjMzMzExUQjh4eHh4OBQwYpqtToyMrLM+5am0+lu375979691NRUrVbr5OTk7+9fr149Y2PjJ9WZn58fExMjhPDy8rKzs3tSsbi4uGvXrqWmpubn51tbW/v6+r722mtPKq9sYAUboufj41OlSpWnKfkMGWJYxsXF3b59e/r06bNmzQoNDW3WrJmyfP/+/du2bVu4cOFrr72mL6xSqfQpVaVKFeXx1KlTXVxcwsLCDh48+Nlnn8XGxlpZWaWlpe3ateu77747f/68paVlcnJyjRo1tmzZsmnTpjVr1tSoUSMiImL+/Pnu7u7Dhg2rWrWqRqNJSUlZtmxZjRo1+vXrp9Vq1Wq1q6vrt99+e/bs2S++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AAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAAnCEgAACcISAAAJwhIAAIn/Bxsa/2jnf+wZAAAAAElFTkSuQmCC\n", 61 | "text/plain": [ 62 | "" 63 | ] 64 | }, 65 | "execution_count": null, 66 | "metadata": {}, 67 | "output_type": "execute_result" 68 | } 69 | ], 70 | "source": [ 71 | "from PIL import Image\n", 72 | "\n", 73 | "filename = get_filename(\"img\", \"0.png\")\n", 74 | "\n", 75 | "Image.open(filename)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "id": "e21660e2", 81 | "metadata": {}, 82 | "source": [ 83 | "## Section 2: Pipeline API" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "id": "ef0b7cb5", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "# pipeline-api\n", 94 | "from paddleocr import PaddleOCR\n", 95 | "\n", 96 | "import logging\n", 97 | "logging.disable()" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "id": "7cb5e00b", 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "# pipeline-api\n", 108 | "from PIL import Image\n", 109 | "import numpy as np\n", 110 | "\n", 111 | "def pipeline_api(\n", 112 | " file,\n", 113 | " file_content_type=None,\n", 114 | " m_some_parameters=[],\n", 115 | "):\n", 116 | " ocr = PaddleOCR(lang=\"en\", use_gpu = False, show_log = False) \n", 117 | " result = ocr.ocr(img=np.array(Image.open(file)))\n", 118 | " \n", 119 | " result =[(p1[0],tuple((p1[1][0],round(p1[1][1],4)))) for p in result for p1 in p]\n", 120 | "\n", 121 | " return json.dumps({ \"result\" : result})\n" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "id": "0400f975", 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "name": "stdout", 132 | "output_type": "stream", 133 | "text": [ 134 | "{'result': [[[[48.0, 24.0], [112.0, 24.0], [112.0, 35.0], [48.0, 35.0]], ['AK Transport', 0.9921]], [[[462.0, 26.0], [576.0, 26.0], [576.0, 50.0], [462.0, 50.0]], ['INVOICE', 0.998]], [[[46.0, 36.0], [126.0, 36.0], [126.0, 49.0], [46.0, 49.0]], ['352 Palmer Road', 0.981]], [[[47.0, 46.0], [76.0, 49.0], [75.0, 61.0], [46.0, 58.0]], ['Ware', 0.9973]], [[[47.0, 60.0], [116.0, 60.0], [116.0, 71.0], [47.0, 71.0]], ['MA, 1082, USA', 0.9995]], [[[520.0, 53.0], [573.0, 56.0], [572.0, 70.0], [519.0, 68.0]], ['#659950', 0.9963]], [[[437.0, 107.0], [466.0, 107.0], [466.0, 122.0], [437.0, 122.0]], ['Date:', 0.9989]], [[[522.0, 105.0], [569.0, 107.0], [568.0, 121.0], [521.0, 119.0]], ['4/11/2020', 1.0]], [[[47.0, 121.0], [79.0, 123.0], [78.0, 134.0], [46.0, 131.0]], ['Bill To:', 0.9944]], [[[45.0, 136.0], [156.0, 138.0], [156.0, 152.0], [45.0, 149.0]], ['Quadrant Lite Planning', 0.9752]], [[[392.0, 130.0], [464.0, 130.0], [464.0, 144.0], [392.0, 144.0]], ['Balance Due:', 0.9757]], [[[525.0, 131.0], [570.0, 131.0], [570.0, 145.0], [525.0, 145.0]], ['$198.30', 0.9956]], [[[46.0, 149.0], [141.0, 149.0], [141.0, 162.0], [46.0, 162.0]], ['3371 S Alabama Ave', 0.9998]], [[[46.0, 161.0], [99.0, 162.0], [99.0, 173.0], [46.0, 172.0]], ['Monroeville', 0.9992]], [[[47.0, 174.0], [119.0, 174.0], [119.0, 185.0], [47.0, 185.0]], ['AL, 36460, USA', 0.9779]], [[[42.0, 225.0], [65.0, 225.0], [65.0, 236.0], [42.0, 236.0]], ['Item', 0.9908]], [[[370.0, 223.0], [411.0, 223.0], [411.0, 237.0], [370.0, 237.0]], ['Quantity', 0.9994]], [[[471.0, 224.0], [494.0, 224.0], [494.0, 236.0], [471.0, 236.0]], ['Rate', 0.9999]], [[[532.0, 225.0], [570.0, 225.0], [570.0, 236.0], [532.0, 236.0]], ['Amount', 0.9999]], [[[43.0, 248.0], [198.0, 248.0], [198.0, 261.0], [43.0, 261.0]], ['Reviva Oatmeal Soap Bar 4.20 oz', 0.9934]], [[[370.0, 249.0], [379.0, 249.0], [379.0, 260.0], [370.0, 260.0]], ['3', 0.9993]], [[[461.0, 248.0], [494.0, 248.0], [494.0, 261.0], [461.0, 261.0]], ['$66.10', 0.9974]], [[[530.0, 248.0], [571.0, 248.0], [571.0, 261.0], [530.0, 261.0]], ['$198.30', 0.997]], [[[438.0, 314.0], [466.0, 314.0], [466.0, 327.0], [438.0, 327.0]], ['Total:', 0.9996]], [[[530.0, 313.0], [570.0, 313.0], [570.0, 327.0], [530.0, 327.0]], ['$198.30', 0.9974]]]}\n" 135 | ] 136 | } 137 | ], 138 | "source": [ 139 | "with open(filename, 'rb') as f:\n", 140 | " result = pipeline_api(f)\n", 141 | "print(json.loads(result))" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": null, 147 | "id": "fcb6a317", 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [] 151 | } 152 | ], 153 | "metadata": { 154 | "kernelspec": { 155 | "display_name": "python3", 156 | "language": "python", 157 | "name": "python3" 158 | } 159 | }, 160 | "nbformat": 4, 161 | "nbformat_minor": 5 162 | } 163 | -------------------------------------------------------------------------------- /prepline_paddleocr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/prepline_paddleocr/__init__.py -------------------------------------------------------------------------------- /prepline_paddleocr/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/prepline_paddleocr/api/__init__.py -------------------------------------------------------------------------------- /prepline_paddleocr/api/app.py: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | # THIS FILE IS AUTOMATICALLY GENERATED BY UNSTRUCTURED API TOOLS. 3 | # DO NOT MODIFY DIRECTLY 4 | ##################################################################### 5 | 6 | 7 | from fastapi import FastAPI, Request, status 8 | 9 | from slowapi import Limiter, _rate_limit_exceeded_handler 10 | from slowapi.errors import RateLimitExceeded 11 | from slowapi.util import get_remote_address 12 | 13 | from .paddleocr import router as paddleocr_router 14 | 15 | 16 | limiter = Limiter(key_func=get_remote_address) 17 | app = FastAPI() 18 | app.state.limiter = limiter 19 | app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) 20 | 21 | app.include_router(paddleocr_router) 22 | 23 | 24 | @app.get("/healthcheck", status_code=status.HTTP_200_OK) 25 | async def healthcheck(request: Request): 26 | return {"healthcheck": "HEALTHCHECK STATUS: EVERYTHING OK!"} 27 | -------------------------------------------------------------------------------- /prepline_paddleocr/api/paddleocr.py: -------------------------------------------------------------------------------- 1 | ##################################################################### 2 | # THIS FILE IS AUTOMATICALLY GENERATED BY UNSTRUCTURED API TOOLS. 3 | # DO NOT MODIFY DIRECTLY 4 | ##################################################################### 5 | 6 | import os 7 | from typing import List, Union 8 | 9 | from fastapi import status, FastAPI, File, Form, Request, UploadFile, APIRouter 10 | from slowapi.errors import RateLimitExceeded 11 | from slowapi import Limiter, _rate_limit_exceeded_handler 12 | from slowapi.util import get_remote_address 13 | from fastapi.responses import PlainTextResponse 14 | 15 | limiter = Limiter(key_func=get_remote_address) 16 | app = FastAPI() 17 | app.state.limiter = limiter 18 | app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) 19 | router = APIRouter() 20 | 21 | RATE_LIMIT = os.environ.get("PIPELINE_API_RATE_LIMIT", "1/second") 22 | 23 | 24 | # pipeline-api 25 | from paddleocr import PaddleOCR 26 | 27 | import logging 28 | 29 | logging.disable() 30 | from PIL import Image 31 | import numpy as np 32 | 33 | 34 | def pipeline_api( 35 | file, 36 | file_content_type=None, 37 | m_some_parameters=[], 38 | ): 39 | ocr = PaddleOCR(lang="en", use_gpu=False, show_log=False) 40 | result = ocr.ocr(img=np.array(Image.open(file))) 41 | 42 | result = [ 43 | (p1[0], tuple((p1[1][0], round(p1[1][1], 4)))) for p in result for p1 in p 44 | ] 45 | 46 | return json.dumps({"result": result}) 47 | 48 | 49 | import json 50 | from fastapi.responses import StreamingResponse 51 | from starlette.types import Send 52 | from base64 import b64encode 53 | from typing import Optional, Mapping, Iterator, Tuple 54 | import secrets 55 | 56 | 57 | class MultipartMixedResponse(StreamingResponse): 58 | CRLF = b"\r\n" 59 | 60 | def __init__(self, *args, content_type: str = None, **kwargs): 61 | super().__init__(*args, **kwargs) 62 | self.content_type = content_type 63 | 64 | def init_headers(self, headers: Optional[Mapping[str, str]] = None) -> None: 65 | super().init_headers(headers) 66 | self.boundary_value = secrets.token_hex(16) 67 | content_type = f'multipart/mixed; boundary="{self.boundary_value}"' 68 | self.raw_headers.append((b"content-type", content_type.encode("latin-1"))) 69 | 70 | @property 71 | def boundary(self): 72 | return b"--" + self.boundary_value.encode() 73 | 74 | def _build_part_headers(self, headers: dict) -> bytes: 75 | header_bytes = b"" 76 | for header, value in headers.items(): 77 | header_bytes += f"{header}: {value}".encode() + self.CRLF 78 | return header_bytes 79 | 80 | def build_part(self, chunk: bytes) -> bytes: 81 | part = self.boundary + self.CRLF 82 | part_headers = { 83 | "Content-Length": len(chunk), 84 | "Content-Transfer-Encoding": "base64", 85 | } 86 | if self.content_type is not None: 87 | part_headers["Content-Type"] = self.content_type 88 | part += self._build_part_headers(part_headers) 89 | part += self.CRLF + chunk + self.CRLF 90 | return part 91 | 92 | async def stream_response(self, send: Send) -> None: 93 | await send( 94 | { 95 | "type": "http.response.start", 96 | "status": self.status_code, 97 | "headers": self.raw_headers, 98 | } 99 | ) 100 | async for chunk in self.body_iterator: 101 | if not isinstance(chunk, bytes): 102 | chunk = chunk.encode(self.charset) 103 | chunk = b64encode(chunk) 104 | await send( 105 | { 106 | "type": "http.response.body", 107 | "body": self.build_part(chunk), 108 | "more_body": True, 109 | } 110 | ) 111 | 112 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 113 | 114 | 115 | @router.post("/paddleocr/v0.0.1/paddleocr") 116 | @limiter.limit(RATE_LIMIT) 117 | async def pipeline_1( 118 | request: Request, 119 | files: Union[List[UploadFile], None] = File(default=None), 120 | some_parameters: List[str] = Form(default=[]), 121 | ): 122 | content_type = request.headers.get("Accept") 123 | 124 | if isinstance(files, list) and len(files): 125 | if len(files) > 1: 126 | if content_type and content_type not in ["*/*", "multipart/mixed"]: 127 | return PlainTextResponse( 128 | content=( 129 | f"Conflict in media type {content_type}" 130 | ' with response type "multipart/mixed".\n' 131 | ), 132 | status_code=status.HTTP_406_NOT_ACCEPTABLE, 133 | ) 134 | 135 | def response_generator(): 136 | for file in files: 137 | _file = file.file 138 | 139 | response = pipeline_api( 140 | _file, 141 | m_some_parameters=some_parameters, 142 | file_content_type=file.content_type, 143 | ) 144 | if type(response) not in [str, bytes]: 145 | response = json.dumps(response) 146 | yield response 147 | 148 | return MultipartMixedResponse( 149 | response_generator(), 150 | ) 151 | else: 152 | file = files[0] 153 | _file = file.file 154 | 155 | response = pipeline_api( 156 | _file, 157 | m_some_parameters=some_parameters, 158 | file_content_type=file.content_type, 159 | ) 160 | 161 | return response 162 | 163 | else: 164 | return PlainTextResponse( 165 | content='Request parameter "files" is required.\n', 166 | status_code=status.HTTP_400_BAD_REQUEST, 167 | ) 168 | 169 | 170 | @app.get("/healthcheck", status_code=status.HTTP_200_OK) 171 | async def healthcheck(request: Request): 172 | return {"healthcheck": "HEALTHCHECK STATUS: EVERYTHING OK!"} 173 | 174 | 175 | app.include_router(router) 176 | -------------------------------------------------------------------------------- /preprocessing-pipeline-family.yaml: -------------------------------------------------------------------------------- 1 | name: paddleocr 2 | version: 0.0.1 3 | -------------------------------------------------------------------------------- /requirements/base.in: -------------------------------------------------------------------------------- 1 | unstructured>=0.2.4 2 | unstructured-api-tools>=0.4.4 3 | 4 | opencv-python==4.5.5.64 5 | pip-tools>=6.11.0 6 | ipython>=8.7.0 7 | ratelimit 8 | 9 | paddlepaddle 10 | paddleocr 11 | werkzeug>=2.2.3 12 | future>=0.18.3 13 | jupyter-core>=4.11.2 14 | nbdev>=2.3.12 15 | #protobuf>=3.20.2 16 | #starlette>=0.25.0 17 | IPython>=8.10 18 | wheel>=0.38.1 19 | pytest>=7.2.0 20 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile requirements/base.in 6 | # 7 | anyio==3.6.1 8 | # via 9 | # starlette 10 | # watchfiles 11 | astor==0.8.1 12 | # via paddlepaddle 13 | asttokens==2.2.1 14 | # via 15 | # nbdev 16 | # stack-data 17 | astunparse==1.6.3 18 | # via nbdev 19 | attrdict==2.0.1 20 | # via paddleocr 21 | attrs==22.1.0 22 | # via 23 | # jsonschema 24 | # pytest 25 | babel==2.11.0 26 | # via flask-babel 27 | backcall==0.2.0 28 | # via ipython 29 | bce-python-sdk==0.8.74 30 | # via visualdl 31 | beautifulsoup4==4.11.1 32 | # via 33 | # nbconvert 34 | # paddleocr 35 | bleach==5.0.1 36 | # via nbconvert 37 | build==0.9.0 38 | # via pip-tools 39 | cachetools==5.2.0 40 | # via premailer 41 | certifi==2022.12.7 42 | # via requests 43 | charset-normalizer==2.1.1 44 | # via requests 45 | click==8.1.3 46 | # via 47 | # flask 48 | # nltk 49 | # pip-tools 50 | # unstructured-api-tools 51 | # uvicorn 52 | contourpy==1.0.6 53 | # via matplotlib 54 | cssselect==1.2.0 55 | # via premailer 56 | cssutils==2.6.0 57 | # via premailer 58 | cycler==0.11.0 59 | # via matplotlib 60 | cython==0.29.32 61 | # via paddleocr 62 | decorator==5.1.1 63 | # via 64 | # ipython 65 | # paddlepaddle 66 | defusedxml==0.7.1 67 | # via nbconvert 68 | dill==0.3.6 69 | # via multiprocess 70 | entrypoints==0.4 71 | # via jupyter-client 72 | et-xmlfile==1.1.0 73 | # via openpyxl 74 | exceptiongroup==1.1.0 75 | # via pytest 76 | execnb==0.1.5 77 | # via nbdev 78 | executing==1.2.0 79 | # via stack-data 80 | fastapi==0.85.0 81 | # via unstructured-api-tools 82 | fastcore==1.5.28 83 | # via 84 | # execnb 85 | # ghapi 86 | # nbdev 87 | fastjsonschema==2.16.2 88 | # via nbformat 89 | fire==0.4.0 90 | # via 91 | # paddleocr 92 | # pdf2docx 93 | flask==2.2.2 94 | # via 95 | # flask-babel 96 | # visualdl 97 | flask-babel==2.0.0 98 | # via visualdl 99 | fonttools==4.38.0 100 | # via 101 | # matplotlib 102 | # paddleocr 103 | # pdf2docx 104 | future==0.18.3 105 | # via 106 | # -r requirements/base.in 107 | # bce-python-sdk 108 | ghapi==1.0.3 109 | # via nbdev 110 | h11==0.13.0 111 | # via uvicorn 112 | httptools==0.5.0 113 | # via uvicorn 114 | idna==3.4 115 | # via 116 | # anyio 117 | # requests 118 | imageio==2.22.4 119 | # via 120 | # imgaug 121 | # scikit-image 122 | imgaug==0.4.0 123 | # via paddleocr 124 | importlib-metadata==5.0.0 125 | # via 126 | # flask 127 | # nbconvert 128 | iniconfig==2.0.0 129 | # via pytest 130 | ipython==8.10.0 131 | # via 132 | # -r requirements/base.in 133 | # execnb 134 | itsdangerous==2.1.2 135 | # via flask 136 | jedi==0.18.2 137 | # via ipython 138 | jinja2==3.1.2 139 | # via 140 | # flask 141 | # flask-babel 142 | # nbconvert 143 | # unstructured-api-tools 144 | joblib==1.2.0 145 | # via nltk 146 | jsonschema==4.16.0 147 | # via nbformat 148 | jupyter-client==7.3.5 149 | # via nbclient 150 | jupyter-core==5.2.0 151 | # via 152 | # -r requirements/base.in 153 | # jupyter-client 154 | # nbconvert 155 | # nbformat 156 | jupyterlab-pygments==0.2.2 157 | # via nbconvert 158 | kiwisolver==1.4.4 159 | # via matplotlib 160 | limits==1.6 161 | # via slowapi 162 | lmdb==1.4.0 163 | # via paddleocr 164 | lxml==4.9.1 165 | # via 166 | # nbconvert 167 | # paddleocr 168 | # premailer 169 | # python-docx 170 | # unstructured 171 | markupsafe==2.1.1 172 | # via 173 | # jinja2 174 | # nbconvert 175 | # werkzeug 176 | matplotlib==3.6.2 177 | # via 178 | # imgaug 179 | # visualdl 180 | matplotlib-inline==0.1.6 181 | # via ipython 182 | mistune==2.0.4 183 | # via nbconvert 184 | multiprocess==0.70.14 185 | # via visualdl 186 | mypy==0.991 187 | # via unstructured-api-tools 188 | mypy-extensions==0.4.3 189 | # via mypy 190 | nbclient==0.6.8 191 | # via nbconvert 192 | nbconvert==7.0.0 193 | # via unstructured-api-tools 194 | nbdev==2.3.12 195 | # via -r requirements/base.in 196 | nbformat==5.6.0 197 | # via 198 | # nbclient 199 | # nbconvert 200 | nest-asyncio==1.5.5 201 | # via 202 | # jupyter-client 203 | # nbclient 204 | networkx==2.8.8 205 | # via scikit-image 206 | nltk==3.7 207 | # via unstructured 208 | numpy==1.23.5 209 | # via 210 | # contourpy 211 | # imageio 212 | # imgaug 213 | # matplotlib 214 | # opencv-contrib-python 215 | # opencv-python 216 | # opt-einsum 217 | # paddleocr 218 | # paddlepaddle 219 | # pandas 220 | # pdf2docx 221 | # pywavelets 222 | # scikit-image 223 | # scipy 224 | # tifffile 225 | # visualdl 226 | opencv-contrib-python==4.6.0.66 227 | # via paddleocr 228 | opencv-python==4.5.5.64 229 | # via 230 | # -r requirements/base.in 231 | # imgaug 232 | # paddleocr 233 | # pdf2docx 234 | openpyxl==3.0.10 235 | # via paddleocr 236 | opt-einsum==3.3.0 237 | # via paddlepaddle 238 | packaging==21.3 239 | # via 240 | # build 241 | # fastcore 242 | # ghapi 243 | # matplotlib 244 | # nbconvert 245 | # pytest 246 | # scikit-image 247 | # visualdl 248 | paddle-bfloat==0.1.7 249 | # via paddlepaddle 250 | paddleocr==2.6.1.1 251 | # via -r requirements/base.in 252 | paddlepaddle==2.4.0 253 | # via -r requirements/base.in 254 | pandas==1.5.2 255 | # via visualdl 256 | pandocfilters==1.5.0 257 | # via nbconvert 258 | parso==0.8.3 259 | # via jedi 260 | pdf2docx==0.5.6 261 | # via paddleocr 262 | pep517==0.13.0 263 | # via build 264 | pexpect==4.8.0 265 | # via ipython 266 | pickleshare==0.7.5 267 | # via ipython 268 | pillow==9.3.0 269 | # via 270 | # imageio 271 | # imgaug 272 | # matplotlib 273 | # paddlepaddle 274 | # scikit-image 275 | # visualdl 276 | pip-tools==6.11.0 277 | # via -r requirements/base.in 278 | platformdirs==3.0.0 279 | # via jupyter-core 280 | pluggy==1.0.0 281 | # via pytest 282 | premailer==3.10.0 283 | # via paddleocr 284 | prompt-toolkit==3.0.36 285 | # via ipython 286 | protobuf==3.20.0 287 | # via 288 | # paddlepaddle 289 | # visualdl 290 | ptyprocess==0.7.0 291 | # via pexpect 292 | pure-eval==0.2.2 293 | # via stack-data 294 | pyclipper==1.3.0.post4 295 | # via paddleocr 296 | pycryptodome==3.16.0 297 | # via bce-python-sdk 298 | pydantic==1.10.2 299 | # via fastapi 300 | pygments==2.13.0 301 | # via 302 | # ipython 303 | # nbconvert 304 | pymupdf==1.20.2 305 | # via 306 | # paddleocr 307 | # pdf2docx 308 | pyparsing==3.0.9 309 | # via 310 | # matplotlib 311 | # packaging 312 | pyrsistent==0.18.1 313 | # via jsonschema 314 | pytest==7.2.1 315 | # via -r requirements/base.in 316 | python-dateutil==2.8.2 317 | # via 318 | # jupyter-client 319 | # matplotlib 320 | # pandas 321 | python-docx==0.8.11 322 | # via 323 | # paddleocr 324 | # pdf2docx 325 | python-dotenv==0.21.0 326 | # via uvicorn 327 | python-multipart==0.0.5 328 | # via unstructured-api-tools 329 | pytz==2022.6 330 | # via 331 | # babel 332 | # flask-babel 333 | # pandas 334 | pywavelets==1.4.1 335 | # via scikit-image 336 | pyyaml==6.0 337 | # via 338 | # nbdev 339 | # uvicorn 340 | pyzmq==24.0.1 341 | # via jupyter-client 342 | rapidfuzz==2.13.3 343 | # via paddleocr 344 | ratelimit==2.2.1 345 | # via -r requirements/base.in 346 | regex==2022.10.31 347 | # via nltk 348 | requests==2.28.1 349 | # via 350 | # paddlepaddle 351 | # premailer 352 | # visualdl 353 | scikit-image==0.19.3 354 | # via 355 | # imgaug 356 | # paddleocr 357 | scipy==1.9.3 358 | # via 359 | # imgaug 360 | # scikit-image 361 | shapely==1.8.5.post1 362 | # via 363 | # imgaug 364 | # paddleocr 365 | six==1.16.0 366 | # via 367 | # asttokens 368 | # astunparse 369 | # attrdict 370 | # bce-python-sdk 371 | # bleach 372 | # fire 373 | # imgaug 374 | # limits 375 | # paddlepaddle 376 | # python-dateutil 377 | # python-multipart 378 | # visualdl 379 | slowapi==0.1.6 380 | # via unstructured-api-tools 381 | sniffio==1.3.0 382 | # via anyio 383 | soupsieve==2.3.2.post1 384 | # via beautifulsoup4 385 | stack-data==0.6.2 386 | # via ipython 387 | starlette==0.20.4 388 | # via fastapi 389 | termcolor==2.1.1 390 | # via fire 391 | tifffile==2022.10.10 392 | # via scikit-image 393 | tinycss2==1.1.1 394 | # via nbconvert 395 | tomli==2.0.1 396 | # via 397 | # build 398 | # mypy 399 | # pep517 400 | # pytest 401 | tornado==6.2 402 | # via jupyter-client 403 | tqdm==4.64.1 404 | # via 405 | # nltk 406 | # paddleocr 407 | traitlets==5.4.0 408 | # via 409 | # ipython 410 | # jupyter-client 411 | # jupyter-core 412 | # matplotlib-inline 413 | # nbclient 414 | # nbconvert 415 | # nbformat 416 | types-requests==2.28.11 417 | # via unstructured-api-tools 418 | types-ujson==5.5.0 419 | # via unstructured-api-tools 420 | types-urllib3==1.26.24 421 | # via types-requests 422 | typing-extensions==4.3.0 423 | # via 424 | # mypy 425 | # pydantic 426 | # starlette 427 | unstructured==0.2.5 428 | # via -r requirements/base.in 429 | unstructured-api-tools==0.4.6 430 | # via -r requirements/base.in 431 | urllib3==1.26.13 432 | # via requests 433 | uvicorn[standard]==0.18.3 434 | # via unstructured-api-tools 435 | uvloop==0.17.0 436 | # via uvicorn 437 | visualdl==2.4.1 438 | # via paddleocr 439 | watchdog==2.2.1 440 | # via nbdev 441 | watchfiles==0.17.0 442 | # via uvicorn 443 | wcwidth==0.2.5 444 | # via prompt-toolkit 445 | webencodings==0.5.1 446 | # via 447 | # bleach 448 | # tinycss2 449 | websockets==10.3 450 | # via uvicorn 451 | werkzeug==2.2.3 452 | # via 453 | # -r requirements/base.in 454 | # flask 455 | wheel==0.38.4 456 | # via 457 | # -r requirements/base.in 458 | # astunparse 459 | # pip-tools 460 | zipp==3.10.0 461 | # via importlib-metadata 462 | 463 | # The following packages are considered to be unsafe in a requirements file: 464 | # pip 465 | # setuptools 466 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | black 2 | flake8 3 | jupyter 4 | mypy 5 | nbdev 6 | pip-tools 7 | # NOTE(crag): consistency with unstructured-api-tools. pinned for a reason, see there. 8 | ipython==8.7.0 9 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile requirements/dev.in 6 | # 7 | argon2-cffi==21.3.0 8 | # via notebook 9 | argon2-cffi-bindings==21.2.0 10 | # via argon2-cffi 11 | asttokens==2.0.8 12 | # via 13 | # nbdev 14 | # stack-data 15 | astunparse==1.6.3 16 | # via nbdev 17 | attrs==22.1.0 18 | # via jsonschema 19 | backcall==0.2.0 20 | # via ipython 21 | beautifulsoup4==4.11.1 22 | # via nbconvert 23 | black==23.1.0 24 | # via -r requirements/dev.in 25 | bleach==5.0.1 26 | # via nbconvert 27 | build==0.8.0 28 | # via pip-tools 29 | cffi==1.15.1 30 | # via argon2-cffi-bindings 31 | click==8.1.3 32 | # via 33 | # black 34 | # pip-tools 35 | debugpy==1.6.3 36 | # via ipykernel 37 | decorator==5.1.1 38 | # via ipython 39 | defusedxml==0.7.1 40 | # via nbconvert 41 | entrypoints==0.4 42 | # via jupyter-client 43 | execnb==0.1.4 44 | # via nbdev 45 | executing==1.0.0 46 | # via stack-data 47 | fastcore==1.5.27 48 | # via 49 | # execnb 50 | # ghapi 51 | # nbdev 52 | fastjsonschema==2.16.2 53 | # via nbformat 54 | flake8==6.0.0 55 | # via -r requirements/dev.in 56 | ghapi==1.0.3 57 | # via nbdev 58 | importlib-metadata==6.0.0 59 | # via nbconvert 60 | ipykernel==6.15.3 61 | # via 62 | # ipywidgets 63 | # jupyter 64 | # jupyter-console 65 | # notebook 66 | # qtconsole 67 | ipython==8.7.0 68 | # via 69 | # -r requirements/dev.in 70 | # execnb 71 | # ipykernel 72 | # ipywidgets 73 | # jupyter-console 74 | ipython-genutils==0.2.0 75 | # via 76 | # notebook 77 | # qtconsole 78 | ipywidgets==8.0.2 79 | # via jupyter 80 | jedi==0.18.1 81 | # via ipython 82 | jinja2==3.1.2 83 | # via 84 | # nbconvert 85 | # notebook 86 | jsonschema==4.16.0 87 | # via nbformat 88 | jupyter==1.0.0 89 | # via -r requirements/dev.in 90 | jupyter-client==7.3.5 91 | # via 92 | # ipykernel 93 | # jupyter-console 94 | # nbclient 95 | # notebook 96 | # qtconsole 97 | jupyter-console==6.4.4 98 | # via jupyter 99 | jupyter-core==4.11.1 100 | # via 101 | # jupyter-client 102 | # nbconvert 103 | # nbformat 104 | # notebook 105 | # qtconsole 106 | jupyterlab-pygments==0.2.2 107 | # via nbconvert 108 | jupyterlab-widgets==3.0.3 109 | # via ipywidgets 110 | lxml==4.9.1 111 | # via nbconvert 112 | markupsafe==2.1.1 113 | # via 114 | # jinja2 115 | # nbconvert 116 | matplotlib-inline==0.1.6 117 | # via 118 | # ipykernel 119 | # ipython 120 | mccabe==0.7.0 121 | # via flake8 122 | mistune==2.0.4 123 | # via nbconvert 124 | mypy==0.991 125 | # via -r requirements/dev.in 126 | mypy-extensions==0.4.3 127 | # via 128 | # black 129 | # mypy 130 | nbclient==0.6.8 131 | # via nbconvert 132 | nbconvert==7.0.0 133 | # via 134 | # jupyter 135 | # notebook 136 | nbdev==2.3.11 137 | # via -r requirements/dev.in 138 | nbformat==5.6.0 139 | # via 140 | # nbclient 141 | # nbconvert 142 | # notebook 143 | nest-asyncio==1.5.5 144 | # via 145 | # ipykernel 146 | # jupyter-client 147 | # nbclient 148 | # notebook 149 | notebook==6.4.12 150 | # via jupyter 151 | packaging==23.0 152 | # via 153 | # black 154 | # build 155 | # fastcore 156 | # ghapi 157 | # ipykernel 158 | # nbconvert 159 | # qtpy 160 | pandocfilters==1.5.0 161 | # via nbconvert 162 | parso==0.8.3 163 | # via jedi 164 | pathspec==0.10.1 165 | # via black 166 | pep517==0.13.0 167 | # via build 168 | pexpect==4.8.0 169 | # via ipython 170 | pickleshare==0.7.5 171 | # via ipython 172 | pip-tools==6.11.0 173 | # via -r requirements/dev.in 174 | platformdirs==2.5.2 175 | # via black 176 | prometheus-client==0.14.1 177 | # via notebook 178 | prompt-toolkit==3.0.31 179 | # via 180 | # ipython 181 | # jupyter-console 182 | psutil==5.9.2 183 | # via ipykernel 184 | ptyprocess==0.7.0 185 | # via 186 | # pexpect 187 | # terminado 188 | pure-eval==0.2.2 189 | # via stack-data 190 | pycodestyle==2.10.0 191 | # via flake8 192 | pycparser==2.21 193 | # via cffi 194 | pyflakes==3.0.1 195 | # via flake8 196 | pygments==2.13.0 197 | # via 198 | # ipython 199 | # jupyter-console 200 | # nbconvert 201 | # qtconsole 202 | pyrsistent==0.18.1 203 | # via jsonschema 204 | python-dateutil==2.8.2 205 | # via jupyter-client 206 | pyyaml==6.0 207 | # via nbdev 208 | pyzmq==24.0.1 209 | # via 210 | # ipykernel 211 | # jupyter-client 212 | # notebook 213 | # qtconsole 214 | qtconsole==5.3.2 215 | # via jupyter 216 | qtpy==2.2.0 217 | # via qtconsole 218 | send2trash==1.8.0 219 | # via notebook 220 | six==1.16.0 221 | # via 222 | # asttokens 223 | # astunparse 224 | # bleach 225 | # python-dateutil 226 | soupsieve==2.3.2.post1 227 | # via beautifulsoup4 228 | stack-data==0.5.0 229 | # via ipython 230 | terminado==0.15.0 231 | # via notebook 232 | tinycss2==1.1.1 233 | # via nbconvert 234 | tomli==2.0.1 235 | # via 236 | # black 237 | # build 238 | # mypy 239 | # pep517 240 | tornado==6.2 241 | # via 242 | # ipykernel 243 | # jupyter-client 244 | # notebook 245 | # terminado 246 | traitlets==5.4.0 247 | # via 248 | # ipykernel 249 | # ipython 250 | # ipywidgets 251 | # jupyter-client 252 | # jupyter-core 253 | # matplotlib-inline 254 | # nbclient 255 | # nbconvert 256 | # nbformat 257 | # notebook 258 | # qtconsole 259 | typing-extensions==4.3.0 260 | # via 261 | # black 262 | # mypy 263 | watchdog==2.1.9 264 | # via nbdev 265 | wcwidth==0.2.5 266 | # via prompt-toolkit 267 | webencodings==0.5.1 268 | # via 269 | # bleach 270 | # tinycss2 271 | wheel==0.37.1 272 | # via 273 | # astunparse 274 | # pip-tools 275 | widgetsnbextension==4.0.3 276 | # via ipywidgets 277 | zipp==3.12.1 278 | # via importlib-metadata 279 | 280 | # The following packages are considered to be unsafe in a requirements file: 281 | # pip 282 | # setuptools 283 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | black 2 | # NOTE(mrobinson) - Pinning click due to a unicode issue in black 3 | # can remove after black drops support for Python 3.6 4 | # ref: https://github.com/psf/black/issues/2964 5 | click==8.1.3 6 | flake8 7 | mypy 8 | pytest-cov 9 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile requirements/test.in 6 | # 7 | attrs==22.1.0 8 | # via pytest 9 | black==23.1.0 10 | # via -r requirements/test.in 11 | click==8.1.3 12 | # via 13 | # -r requirements/test.in 14 | # black 15 | coverage[toml]==6.4.4 16 | # via pytest-cov 17 | flake8==6.0.0 18 | # via -r requirements/test.in 19 | iniconfig==1.1.1 20 | # via pytest 21 | mccabe==0.7.0 22 | # via flake8 23 | mypy==0.991 24 | # via -r requirements/test.in 25 | mypy-extensions==0.4.3 26 | # via 27 | # black 28 | # mypy 29 | packaging==23.0 30 | # via 31 | # black 32 | # pytest 33 | pathspec==0.10.1 34 | # via black 35 | platformdirs==2.5.2 36 | # via black 37 | pluggy==1.0.0 38 | # via pytest 39 | py==1.11.0 40 | # via pytest 41 | pycodestyle==2.10.0 42 | # via flake8 43 | pyflakes==3.0.1 44 | # via flake8 45 | pytest==7.1.3 46 | # via pytest-cov 47 | pytest-cov==4.0.0 48 | # via -r requirements/test.in 49 | tomli==2.0.1 50 | # via 51 | # black 52 | # coverage 53 | # mypy 54 | # pytest 55 | typing-extensions==4.3.0 56 | # via 57 | # black 58 | # mypy 59 | -------------------------------------------------------------------------------- /sample-docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/sample-docs/.gitkeep -------------------------------------------------------------------------------- /sample-docs/sample-receipt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/sample-docs/sample-receipt.jpg -------------------------------------------------------------------------------- /scripts/check-and-format-notebooks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | from copy import deepcopy 5 | import difflib 6 | import json 7 | from pathlib import Path 8 | import sys 9 | from typing import List, Tuple, Union 10 | 11 | from nbdev import clean 12 | from nbconvert.preprocessors import ExecutePreprocessor 13 | import nbformat 14 | from unstructured_api_tools.pipelines.convert import read_notebook 15 | 16 | 17 | def process_nb(nb: nbformat.NotebookNode, working_dir: Union[str, Path]) -> nbformat.NotebookNode: 18 | """Execute cells in nb using working_dir as the working directory for imports, modifying the 19 | notebook in place (in memory).""" 20 | # Clear existing outputs before executing the notebook 21 | for cell in nb.cells: 22 | if cell.cell_type == "code": 23 | cell.outputs = [] 24 | ep = ExecutePreprocessor(timeout=600) 25 | ep.preprocess(nb, {"metadata": {"path": working_dir}}) 26 | # Merge adjacent text outputs after executing the notebook 27 | for cell in nb.cells: 28 | merge_adjacent_text_outputs(cell) 29 | return nb 30 | 31 | 32 | def merge_adjacent_text_outputs(cell: nbformat.NotebookNode) -> nbformat.NotebookNode: 33 | """Merges adjacent text stream outputs to avoid non-deterministic splits in output.""" 34 | if cell.cell_type != "code": 35 | return cell 36 | 37 | new_outputs = [] 38 | current_output = None 39 | 40 | for output in cell.outputs: 41 | if output.output_type == "stream": 42 | if current_output is None: 43 | current_output = output 44 | elif current_output.name == output.name: 45 | current_output.text += output.text 46 | else: 47 | new_outputs.append(current_output) 48 | current_output = output 49 | else: 50 | if current_output is not None: 51 | new_outputs.append(current_output) 52 | current_output = None 53 | new_outputs.append(output) 54 | 55 | if current_output is not None: 56 | new_outputs.append(current_output) 57 | 58 | cell.outputs = new_outputs 59 | return cell 60 | 61 | 62 | def nb_paths(root_path: Union[str, Path]) -> List[Path]: 63 | """Fetches all .ipynb filenames that belong to subdirectories of root_path (1 level deep) with 64 | 'notebooks' in the name.""" 65 | root_path = Path(root_path) 66 | return [ 67 | fn 68 | for dir in root_path.iterdir() 69 | # NOTE(alan): Search only in paths with 'notebooks' in the title such as pipeline-notebooks 70 | # and exploration-notebooks 71 | if "notebooks" in dir.stem and dir.is_dir() 72 | for fn in dir.iterdir() 73 | if fn.suffix == ".ipynb" 74 | ] 75 | 76 | 77 | def to_results_str(fns: List[Path], nonmatching_nbs: List[Path]) -> Tuple[str, str]: 78 | """Given files that were checked and list of files that would be changed, produces a summary of 79 | changes as well as a list of files to be changed""" 80 | unchanged = len(fns) - len(nonmatching_nbs) 81 | results = [] 82 | if nonmatching_nbs: 83 | results.append( 84 | f"{len(nonmatching_nbs)} " 85 | f"{'file' if len(nonmatching_nbs) == 1 else 'files'} " 86 | f"{'would be ' if check else ''}changed" 87 | ) 88 | if unchanged: 89 | results.append( 90 | f"{unchanged} " 91 | f"{'file' if unchanged == 1 else 'files'} " 92 | f"{'would be ' if check else ''}left unchanged" 93 | ) 94 | summary_str = ", ".join(results) + ".\n" 95 | if nonmatching_nbs: 96 | details_str = ( 97 | f"The following notebooks {'would have been' if check else 'were'} " 98 | "changed when executed and cleaned:\n* " + "\n* ".join(nonmatching_nbs) + "\n" 99 | ) 100 | else: 101 | details_str = "" 102 | 103 | return summary_str, details_str 104 | 105 | 106 | if __name__ == "__main__": 107 | parser = argparse.ArgumentParser() 108 | parser.add_argument( 109 | "--check", 110 | default=False, 111 | action="store_true", 112 | help="Check notebook format without making changes. Return code 0 means formatting would " 113 | "produce no changes. Return code 1 means some files would be changed.", 114 | ) 115 | parser.add_argument( 116 | "notebooks", 117 | metavar="notebook", 118 | nargs="*", 119 | help="Path(s) to notebook(s) to format (or check). If you don't pass any paths, " 120 | "notebooks in any subfolders with 'notebooks' in the name will be processed.", 121 | default=[], 122 | ) 123 | args = parser.parse_args() 124 | check = args.check 125 | notebooks = args.notebooks 126 | 127 | root_path = Path(__file__).parent.parent 128 | nonmatching_nbs = [] 129 | fns = notebooks if notebooks else nb_paths(root_path) 130 | for fn in fns: 131 | print(f"{'checking' if check else 'processing'} {fn}") 132 | nb = read_notebook(fn) 133 | modified_nb = deepcopy(nb) 134 | process_nb(modified_nb, root_path) 135 | clean.clean_nb(modified_nb, allowed_cell_metadata_keys=["tags"]) 136 | if nb != modified_nb: 137 | nonmatching_nbs.append(str(fn)) 138 | nb_json = json.dumps(nb.dict(), indent=2, sort_keys=True) 139 | modified_nb_json = json.dumps(modified_nb.dict(), indent=2, sort_keys=True) 140 | sys.stderr.write(f"The following diff shows the modifications made to {fn}\n") 141 | sys.stderr.writelines( 142 | ( 143 | difflib.unified_diff( 144 | nb_json.splitlines(keepends=True), 145 | modified_nb_json.splitlines(keepends=True), 146 | ) 147 | ) 148 | ) 149 | if not check: 150 | nbformat.write(modified_nb, fn) 151 | 152 | summary_str, details_str = to_results_str(fns, nonmatching_nbs) 153 | print(summary_str) 154 | if check: 155 | sys.stderr.write(details_str) 156 | if nonmatching_nbs: 157 | sys.exit(1) 158 | else: 159 | print(details_str) 160 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | DOCKER_BUILDKIT=1 docker buildx build --load --platform=linux/amd64 -f Dockerfile \ 6 | --build-arg PIP_VERSION="$PIP_VERSION" \ 7 | --build-arg PIPELINE_PACKAGE="$PIPELINE_PACKAGE" \ 8 | --progress plain \ 9 | -t pipeline-family-"$PIPELINE_FAMILY"-dev:latest . 10 | -------------------------------------------------------------------------------- /scripts/shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | find scripts -name "*.sh" -exec shellcheck {} + 4 | 5 | -------------------------------------------------------------------------------- /scripts/test-doc-pipeline-apis-consistent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu -o pipefail 4 | 5 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 6 | cd "$SCRIPT_DIR"/.. 7 | 8 | PIPELINE_OUTPUT_DIR=tmp-api-check-output-$RANDOM 9 | FILE_INDICTATING_FAILURE="$PIPELINE_OUTPUT_DIR"-has-failures 10 | mkdir -p $PIPELINE_OUTPUT_DIR 11 | touch $PIPELINE_OUTPUT_DIR/__init__.py 12 | 13 | function tmp_pipeline_comp_cleanup () { 14 | cd "$SCRIPT_DIR"/.. 15 | rm -f "$FILE_INDICTATING_FAILURE" 16 | if [[ "$1" -eq 0 ]]; then 17 | rm -rf $PIPELINE_OUTPUT_DIR 18 | fi 19 | exit "$1" 20 | } 21 | 22 | unstructured_api_tools convert-pipeline-notebooks \ 23 | --input-directory ./pipeline-notebooks \ 24 | --output-directory "$PIPELINE_OUTPUT_DIR" 25 | 26 | NUM_PIPELINE_API_FILES_GENERATED=$(find "$PIPELINE_OUTPUT_DIR" -name "*.py" | wc -l) 27 | 28 | if [[ "$NUM_PIPELINE_API_FILES_GENERATED" -eq 0 ]]; then 29 | echo "No pipelines where created by unstructured_api_tools convert-pipeline-notebooks" 30 | tmp_pipeline_comp_cleanup 1 31 | fi 32 | 33 | NUM_EXISTING_PIPELINE_API_FILES=$(find "$PACKAGE_NAME"/api -name "*.py" | wc -l) 34 | 35 | if [[ "$NUM_PIPELINE_API_FILES_GENERATED" -gt "$NUM_EXISTING_PIPELINE_API_FILES" ]]; then 36 | echo "More pipeline api files were autogenerated than appear in the ${PACKAGE_NAME}/api" 37 | tmp_pipeline_comp_cleanup 1 38 | elif [[ "$NUM_PIPELINE_API_FILES_GENERATED" -lt "$NUM_EXISTING_PIPELINE_API_FILES" ]]; then 39 | echo "Fewer pipeline api files were autogenerated than appear in the ${PACKAGE_NAME}/api" 40 | tmp_pipeline_comp_cleanup 1 41 | fi 42 | 43 | cd "$PACKAGE_NAME"/api 44 | find . -name "*.py" -print0 | while IFS= read -r -d '' pipeline_file; do 45 | set +o pipefail 46 | if ! diff -u "$pipeline_file" ../../"$PIPELINE_OUTPUT_DIR/$pipeline_file"; then 47 | touch "../../$FILE_INDICTATING_FAILURE" 48 | fi 49 | set -o pipefail 50 | done 51 | cd - 52 | 53 | if [ -r "$FILE_INDICTATING_FAILURE" ]; then 54 | echo 55 | echo "Autogenerated pipeline api file(s) do not match existing versions, see above for diff's" 56 | echo " or run: diff -ru ${PACKAGE_NAME}/api/ ${PIPELINE_OUTPUT_DIR}/" 57 | tmp_pipeline_comp_cleanup 1 58 | fi 59 | tmp_pipeline_comp_cleanup 0 60 | -------------------------------------------------------------------------------- /scripts/version-sync.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | function usage { 3 | echo "Usage: $(basename "$0") [-c] -f FILE_TO_CHANGE REPLACEMENT_FORMAT [-f FILE_TO_CHANGE REPLACEMENT_FORMAT ...]" 2>&1 4 | echo 'Synchronize files to latest version in source file' 5 | echo ' -s Specifies source file for version (default is CHANGELOG.md)' 6 | echo ' -f Specifies a file to change and the format for searching and replacing versions' 7 | echo ' FILE_TO_CHANGE is the file to be updated/checked for updates' 8 | echo ' REPLACEMENT_FORMAT is one of (semver, release, api-release)' 9 | echo ' semver indicates to look for a full semver version and replace with the latest full version' 10 | echo ' release indicates to look for a release semver version (x.x.x) and replace with the latest release version' 11 | echo ' api-release indicates to look for a release semver version in the context of an api route and replace with the latest release version' 12 | echo ' -c Compare versions and output proposed changes without changing anything.' 13 | } 14 | 15 | function getopts-extra () { 16 | declare i=1 17 | # if the next argument is not an option, then append it to array OPTARG 18 | while [[ ${OPTIND} -le $# && ${!OPTIND:0:1} != '-' ]]; do 19 | OPTARG[i]=${!OPTIND} 20 | i+=1 21 | OPTIND+=1 22 | done 23 | } 24 | 25 | # Parse input options 26 | declare CHECK=0 27 | declare SOURCE_FILE="CHANGELOG.md" 28 | declare -a FILES_TO_CHECK=() 29 | declare -a REPLACEMENT_FORMATS=() 30 | declare args 31 | declare OPTIND OPTARG opt 32 | while getopts ":hcs:f:" opt; do 33 | case $opt in 34 | h) 35 | usage 36 | exit 0 37 | ;; 38 | c) 39 | CHECK=1 40 | ;; 41 | s) 42 | SOURCE_FILE="$OPTARG" 43 | ;; 44 | f) 45 | getopts-extra "$@" 46 | args=( "${OPTARG[@]}" ) 47 | # validate length of args, should be 2 48 | if [ ${#args[@]} -eq 2 ]; then 49 | FILES_TO_CHECK+=( "${args[0]}" ) 50 | REPLACEMENT_FORMATS+=( "${args[1]}" ) 51 | else 52 | echo "Exactly 2 arguments must follow -f option." >&2 53 | exit 1 54 | fi 55 | ;; 56 | \?) 57 | echo "Invalid option: -$OPTARG." >&2 58 | usage 59 | exit 1 60 | ;; 61 | esac 62 | done 63 | 64 | # Parse REPLACEMENT_FORMATS 65 | RE_SEMVER_FULL="(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?" 66 | RE_RELEASE="(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)" 67 | RE_API_RELEASE="v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)" 68 | # Pull out semver appearing earliest in SOURCE_FILE. 69 | LAST_VERSION=$(grep -o -m 1 -E "${RE_SEMVER_FULL}" "$SOURCE_FILE") 70 | LAST_RELEASE=$(grep -o -m 1 -E "${RE_RELEASE}($|[^-+])" "$SOURCE_FILE" | grep -o -m 1 -E "${RE_RELEASE}") 71 | LAST_API_RELEASE="v$(grep -o -m 1 -E "${RE_RELEASE}($|[^-+])$" "$SOURCE_FILE" | grep -o -m 1 -E "${RE_RELEASE}")" 72 | declare -a RE_SEMVERS=() 73 | declare -a UPDATED_VERSIONS=() 74 | for i in "${!REPLACEMENT_FORMATS[@]}"; do 75 | REPLACEMENT_FORMAT=${REPLACEMENT_FORMATS[$i]} 76 | case $REPLACEMENT_FORMAT in 77 | semver) 78 | RE_SEMVERS+=( "$RE_SEMVER_FULL" ) 79 | UPDATED_VERSIONS+=( "$LAST_VERSION" ) 80 | ;; 81 | release) 82 | RE_SEMVERS+=( "$RE_RELEASE" ) 83 | UPDATED_VERSIONS+=( "$LAST_RELEASE" ) 84 | ;; 85 | api-release) 86 | RE_SEMVERS+=( "$RE_API_RELEASE" ) 87 | UPDATED_VERSIONS+=( "$LAST_API_RELEASE" ) 88 | ;; 89 | *) 90 | echo "Invalid replacement format: \"${REPLACEMENT_FORMAT}\". Use semver, release, or api-release" >&2 91 | exit 1 92 | ;; 93 | esac 94 | done 95 | 96 | if [ -z "$LAST_VERSION" ]; 97 | then 98 | # No match to semver regex in SOURCE_FILE, so no version to go from. 99 | printf "Error: Unable to find latest version from %s.\n" "$SOURCE_FILE" 100 | exit 1 101 | fi 102 | 103 | # Search files in FILES_TO_CHECK and change (or get diffs) 104 | declare FAILED_CHECK=0 105 | 106 | for i in "${!FILES_TO_CHECK[@]}"; do 107 | FILE_TO_CHANGE=${FILES_TO_CHECK[$i]} 108 | RE_SEMVER=${RE_SEMVERS[$i]} 109 | UPDATED_VERSION=${UPDATED_VERSIONS[$i]} 110 | FILE_VERSION=$(grep -o -m 1 -E "${RE_SEMVER}" "$FILE_TO_CHANGE") 111 | if [ -z "$FILE_VERSION" ]; 112 | then 113 | # No match to semver regex in VERSIONFILE, so nothing to replace 114 | printf "Error: No semver version found in file %s.\n" "$FILE_TO_CHANGE" 115 | exit 1 116 | else 117 | # Replace semver in VERSIONFILE with semver obtained from SOURCE_FILE 118 | TMPFILE=$(mktemp /tmp/new_version.XXXXXX) 119 | # Check sed version, exit if version < 4.3 120 | if ! sed --version > /dev/null 2>&1; then 121 | CURRENT_VERSION=1.archaic 122 | else 123 | CURRENT_VERSION=$(sed --version | head -n1 | cut -d" " -f4) 124 | fi 125 | REQUIRED_VERSION="4.3" 126 | if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$CURRENT_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then 127 | echo "sed version must be >= ${REQUIRED_VERSION}" && exit 1 128 | fi 129 | sed -E -r "s/$RE_SEMVER/$UPDATED_VERSION/" "$FILE_TO_CHANGE" > "$TMPFILE" 130 | if [ $CHECK == 1 ]; 131 | then 132 | DIFF=$(diff "$FILE_TO_CHANGE" "$TMPFILE" ) 133 | if [ -z "$DIFF" ]; 134 | then 135 | printf "version sync would make no changes to %s.\n" "$FILE_TO_CHANGE" 136 | rm "$TMPFILE" 137 | else 138 | FAILED_CHECK=1 139 | printf "version sync would make the following changes to %s:\n%s\n" "$FILE_TO_CHANGE" "$DIFF" 140 | rm "$TMPFILE" 141 | fi 142 | else 143 | cp "$TMPFILE" "$FILE_TO_CHANGE" 144 | rm "$TMPFILE" 145 | fi 146 | fi 147 | done 148 | 149 | # Exit with code determined by whether changes were needed in a check. 150 | if [ ${FAILED_CHECK} -ne 0 ]; then 151 | exit 1 152 | else 153 | exit 0 154 | fi 155 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = 4 | prepline_*/api 5 | -------------------------------------------------------------------------------- /test_paddleocr/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unstructured-IO/pipeline-paddleocr/7142d2bd80b1687424fef0437f18de6a16124b1b/test_paddleocr/api/.gitkeep -------------------------------------------------------------------------------- /test_paddleocr/api/test_paddleocr.py: -------------------------------------------------------------------------------- 1 | from fastapi.testclient import TestClient 2 | 3 | 4 | from prepline_paddleocr.api.app import app 5 | 6 | 7 | def test_api_health_check(): 8 | client = TestClient(app) 9 | response = client.get("/healthcheck") 10 | 11 | assert response.status_code == 200 12 | 13 | 14 | def test_api_call(): 15 | client = TestClient(app) 16 | with open("img/0.png", "rb") as f: 17 | response = client.post( 18 | "/paddleocr/v0.0.1/paddleocr", files={"files": ("filename", f, "image/jpeg")} 19 | ) 20 | 21 | assert response.status_code == 200 22 | 23 | 24 | def test_api_call_files(): 25 | client = TestClient(app) 26 | 27 | files = [("files", open("img/0.png", "rb")), ("files", open("img/0.png", "rb"))] 28 | 29 | response = client.post("/paddleocr/v0.0.1/paddleocr", files=files) 30 | 31 | assert response.status_code == 200 32 | --------------------------------------------------------------------------------