feat: AutoOCR model selecting the best OCR model available and deprecating the usage of EasyOCR (#2391)

* add auto ocr model

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>

* Apply suggestions from code review

Co-authored-by: Christoph Auer <60343111+cau-git@users.noreply.github.com>
Signed-off-by: Michele Dolfi <97102151+dolfim-ibm@users.noreply.github.com>

* add final log warning

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>

* propagate default options

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>

* allow rapidocr models download

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>

* remove modelscope

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>

---------

Signed-off-by: Michele Dolfi <dol@zurich.ibm.com>
Signed-off-by: Michele Dolfi <97102151+dolfim-ibm@users.noreply.github.com>
Co-authored-by: Christoph Auer <60343111+cau-git@users.noreply.github.com>
This commit is contained in:
Michele Dolfi
2025-10-10 16:11:39 +02:00
committed by GitHub
parent cce18b2ff7
commit f7244a4333
9 changed files with 307 additions and 16 deletions

View File

@@ -49,7 +49,7 @@ from docling.datamodel.document import ConversionResult
from docling.datamodel.pipeline_options import (
AsrPipelineOptions,
ConvertPipelineOptions,
EasyOcrOptions,
OcrAutoOptions,
OcrOptions,
PaginatedPipelineOptions,
PdfBackend,
@@ -374,7 +374,7 @@ def convert( # noqa: C901
f"Use the option --show-external-plugins to see the options allowed with external plugins."
),
),
] = EasyOcrOptions.kind,
] = OcrAutoOptions.kind,
ocr_lang: Annotated[
Optional[str],
typer.Option(

View File

@@ -38,6 +38,7 @@ class _AvailableModels(str, Enum):
SMOLDOCLING = "smoldocling"
SMOLDOCLING_MLX = "smoldocling_mlx"
GRANITE_VISION = "granite_vision"
RAPIDOCR = "rapidocr"
EASYOCR = "easyocr"
@@ -46,7 +47,7 @@ _default_models = [
_AvailableModels.TABLEFORMER,
_AvailableModels.CODE_FORMULA,
_AvailableModels.PICTURE_CLASSIFIER,
_AvailableModels.EASYOCR,
_AvailableModels.RAPIDOCR,
]
@@ -115,6 +116,7 @@ def download(
with_smoldocling=_AvailableModels.SMOLDOCLING in to_download,
with_smoldocling_mlx=_AvailableModels.SMOLDOCLING_MLX in to_download,
with_granite_vision=_AvailableModels.GRANITE_VISION in to_download,
with_rapidocr=_AvailableModels.RAPIDOCR in to_download,
with_easyocr=_AvailableModels.EASYOCR in to_download,
)

View File

@@ -81,6 +81,13 @@ class OcrOptions(BaseOptions):
)
class OcrAutoOptions(OcrOptions):
"""Options for pick OCR engine automatically."""
kind: ClassVar[Literal["auto"]] = "auto"
lang: List[str] = []
class RapidOcrOptions(OcrOptions):
"""Options for the RapidOCR engine."""
@@ -255,6 +262,7 @@ class PdfBackend(str, Enum):
class OcrEngine(str, Enum):
"""Enum of valid OCR engines."""
AUTO = "auto"
EASYOCR = "easyocr"
TESSERACT_CLI = "tesseract_cli"
TESSERACT = "tesseract"
@@ -336,7 +344,7 @@ class PdfPipelineOptions(PaginatedPipelineOptions):
# If True, text from backend will be used instead of generated text
table_structure_options: TableStructureOptions = TableStructureOptions()
ocr_options: OcrOptions = EasyOcrOptions()
ocr_options: OcrOptions = OcrAutoOptions()
layout_options: LayoutOptions = LayoutOptions()
images_scale: float = 1.0

View File

@@ -0,0 +1,132 @@
import logging
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import Optional, Type
from docling.datamodel.accelerator_options import AcceleratorOptions
from docling.datamodel.base_models import Page
from docling.datamodel.document import ConversionResult
from docling.datamodel.pipeline_options import (
EasyOcrOptions,
OcrAutoOptions,
OcrMacOptions,
OcrOptions,
RapidOcrOptions,
)
from docling.models.base_ocr_model import BaseOcrModel
from docling.models.easyocr_model import EasyOcrModel
from docling.models.ocr_mac_model import OcrMacModel
from docling.models.rapid_ocr_model import RapidOcrModel
_log = logging.getLogger(__name__)
class OcrAutoModel(BaseOcrModel):
def __init__(
self,
enabled: bool,
artifacts_path: Optional[Path],
options: OcrAutoOptions,
accelerator_options: AcceleratorOptions,
):
super().__init__(
enabled=enabled,
artifacts_path=artifacts_path,
options=options,
accelerator_options=accelerator_options,
)
self.options: OcrAutoOptions
self._engine: Optional[BaseOcrModel] = None
if self.enabled:
if "darwin" == sys.platform:
try:
from ocrmac import ocrmac
self._engine = OcrMacModel(
enabled=self.enabled,
artifacts_path=artifacts_path,
options=OcrMacOptions(
bitmap_area_threshold=self.options.bitmap_area_threshold,
force_full_page_ocr=self.options.force_full_page_ocr,
),
accelerator_options=accelerator_options,
)
_log.info("Auto OCR model selected ocrmac.")
except ImportError:
_log.info("ocrmac cannot be used because ocrmac is not installed.")
if self._engine is None:
try:
import onnxruntime
from rapidocr import EngineType, RapidOCR # type: ignore
self._engine = RapidOcrModel(
enabled=self.enabled,
artifacts_path=artifacts_path,
options=RapidOcrOptions(
backend="onnxruntime",
bitmap_area_threshold=self.options.bitmap_area_threshold,
force_full_page_ocr=self.options.force_full_page_ocr,
),
accelerator_options=accelerator_options,
)
_log.info("Auto OCR model selected rapidocr with onnxruntime.")
except ImportError:
_log.info(
"rapidocr cannot be used because onnxruntime is not installed."
)
if self._engine is None:
try:
import easyocr
self._engine = EasyOcrModel(
enabled=self.enabled,
artifacts_path=artifacts_path,
options=EasyOcrOptions(
bitmap_area_threshold=self.options.bitmap_area_threshold,
force_full_page_ocr=self.options.force_full_page_ocr,
),
accelerator_options=accelerator_options,
)
_log.info("Auto OCR model selected easyocr.")
except ImportError:
_log.info("easyocr cannot be used because it is not installed.")
if self._engine is None:
try:
import torch
from rapidocr import EngineType, RapidOCR # type: ignore
self._engine = RapidOcrModel(
enabled=self.enabled,
artifacts_path=artifacts_path,
options=RapidOcrOptions(
backend="torch",
bitmap_area_threshold=self.options.bitmap_area_threshold,
force_full_page_ocr=self.options.force_full_page_ocr,
),
accelerator_options=accelerator_options,
)
_log.info("Auto OCR model selected rapidocr with torch.")
except ImportError:
_log.info(
"rapidocr cannot be used because rapidocr or torch is not installed."
)
if self._engine is None:
_log.warning("No OCR engine found. Please review the install details.")
def __call__(
self, conv_res: ConversionResult, page_batch: Iterable[Page]
) -> Iterable[Page]:
if not self.enabled or self._engine is None:
yield from page_batch
return
yield from self._engine(conv_res, page_batch)
@classmethod
def get_options_type(cls) -> Type[OcrOptions]:
return OcrAutoOptions

View File

@@ -1,4 +1,5 @@
def ocr_engines():
from docling.models.auto_ocr_model import OcrAutoModel
from docling.models.easyocr_model import EasyOcrModel
from docling.models.ocr_mac_model import OcrMacModel
from docling.models.rapid_ocr_model import RapidOcrModel
@@ -7,6 +8,7 @@ def ocr_engines():
return {
"ocr_engines": [
OcrAutoModel,
EasyOcrModel,
OcrMacModel,
RapidOcrModel,

View File

@@ -1,7 +1,7 @@
import logging
from collections.abc import Iterable
from pathlib import Path
from typing import Optional, Type
from typing import Literal, Optional, Type, TypedDict
import numpy
from docling_core.types.doc import BoundingBox, CoordOrigin
@@ -18,11 +18,67 @@ from docling.datamodel.settings import settings
from docling.models.base_ocr_model import BaseOcrModel
from docling.utils.accelerator_utils import decide_device
from docling.utils.profiling import TimeRecorder
from docling.utils.utils import download_url_with_progress
_log = logging.getLogger(__name__)
_ModelPathEngines = Literal["onnxruntime", "torch"]
_ModelPathTypes = Literal[
"det_model_path", "cls_model_path", "rec_model_path", "rec_keys_path"
]
class _ModelPathDetail(TypedDict):
url: str
path: str
class RapidOcrModel(BaseOcrModel):
_model_repo_folder = "RapidOcr"
# from https://github.com/RapidAI/RapidOCR/blob/main/python/rapidocr/default_models.yaml
# matching the default config in https://github.com/RapidAI/RapidOCR/blob/main/python/rapidocr/config.yaml
# and naming f"{file_info.engine_type.value}.{file_info.ocr_version.value}.{file_info.task_type.value}"
_default_models: dict[
_ModelPathEngines, dict[_ModelPathTypes, _ModelPathDetail]
] = {
"onnxruntime": {
"det_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/onnx/PP-OCRv4/det/ch_PP-OCRv4_det_infer.onnx",
"path": "onnx/PP-OCRv4/det/ch_PP-OCRv4_det_infer.onnx",
},
"cls_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/onnx/PP-OCRv4/cls/ch_ppocr_mobile_v2.0_cls_infer.onnx",
"path": "onnx/PP-OCRv4/cls/ch_ppocr_mobile_v2.0_cls_infer.onnx",
},
"rec_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/onnx/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer.onnx",
"path": "onnx/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer.onnx",
},
"rec_keys_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v2.0.7/paddle/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer/ppocr_keys_v1.txt",
"path": "paddle/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer/ppocr_keys_v1.txt",
},
},
"torch": {
"det_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/det/ch_PP-OCRv4_det_infer.pth",
"path": "torch/PP-OCRv4/det/ch_PP-OCRv4_det_infer.pth",
},
"cls_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/cls/ch_ptocr_mobile_v2.0_cls_infer.pth",
"path": "torch/PP-OCRv4/cls/ch_ptocr_mobile_v2.0_cls_infer.pth",
},
"rec_model_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/torch/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer.pth",
"path": "torch/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer.pth",
},
"rec_keys_path": {
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/v3.4.0/paddle/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer/ppocr_keys_v1.txt",
"path": "paddle/PP-OCRv4/rec/ch_PP-OCRv4_rec_infer/ppocr_keys_v1.txt",
},
},
}
def __init__(
self,
enabled: bool,
@@ -62,25 +118,66 @@ class RapidOcrModel(BaseOcrModel):
}
backend_enum = _ALIASES.get(self.options.backend, EngineType.ONNXRUNTIME)
det_model_path = self.options.det_model_path
cls_model_path = self.options.cls_model_path
rec_model_path = self.options.rec_model_path
rec_keys_path = self.options.rec_keys_path
if artifacts_path is not None:
det_model_path = (
det_model_path
or artifacts_path
/ self._model_repo_folder
/ self._default_models[backend_enum.value]["det_model_path"]["path"]
)
cls_model_path = (
cls_model_path
or artifacts_path
/ self._model_repo_folder
/ self._default_models[backend_enum.value]["cls_model_path"]["path"]
)
rec_model_path = (
rec_model_path
or artifacts_path
/ self._model_repo_folder
/ self._default_models[backend_enum.value]["rec_model_path"]["path"]
)
rec_keys_path = (
rec_keys_path
or artifacts_path
/ self._model_repo_folder
/ self._default_models[backend_enum.value]["rec_keys_path"]["path"]
)
for model_path in (
rec_keys_path,
cls_model_path,
rec_model_path,
rec_keys_path,
):
if model_path is None:
continue
if not Path(model_path).exists():
_log.warning(f"The provided model path {model_path} is not found.")
params = {
# Global settings (these are still correct)
"Global.text_score": self.options.text_score,
"Global.font_path": self.options.font_path,
# "Global.verbose": self.options.print_verbose,
# Detection model settings
"Det.model_path": self.options.det_model_path,
"Det.model_path": det_model_path,
"Det.use_cuda": use_cuda,
"Det.use_dml": use_dml,
"Det.intra_op_num_threads": intra_op_num_threads,
# Classification model settings
"Cls.model_path": self.options.cls_model_path,
"Cls.model_path": cls_model_path,
"Cls.use_cuda": use_cuda,
"Cls.use_dml": use_dml,
"Cls.intra_op_num_threads": intra_op_num_threads,
# Recognition model settings
"Rec.model_path": self.options.rec_model_path,
"Rec.model_path": rec_model_path,
"Rec.font_path": self.options.rec_font_path,
"Rec.keys_path": self.options.rec_keys_path,
"Rec.keys_path": rec_keys_path,
"Rec.use_cuda": use_cuda,
"Rec.use_dml": use_dml,
"Rec.intra_op_num_threads": intra_op_num_threads,
@@ -102,6 +199,30 @@ class RapidOcrModel(BaseOcrModel):
params=params,
)
@staticmethod
def download_models(
backend: _ModelPathEngines,
local_dir: Optional[Path] = None,
force: bool = False,
progress: bool = False,
) -> Path:
if local_dir is None:
local_dir = settings.cache_dir / "models" / RapidOcrModel._model_repo_folder
local_dir.mkdir(parents=True, exist_ok=True)
# Download models
for model_type, model_details in RapidOcrModel._default_models[backend].items():
output_path = local_dir / model_details["path"]
if output_path.exists() and not force:
continue
output_path.parent.mkdir(exist_ok=True, parents=True)
buf = download_url_with_progress(model_details["url"], progress=progress)
with output_path.open("wb") as fw:
fw.write(buf.read())
return local_dir
def __call__(
self, conv_res: ConversionResult, page_batch: Iterable[Page]
) -> Iterable[Page]:

View File

@@ -20,6 +20,7 @@ from docling.models.document_picture_classifier import DocumentPictureClassifier
from docling.models.easyocr_model import EasyOcrModel
from docling.models.layout_model import LayoutModel
from docling.models.picture_description_vlm_model import PictureDescriptionVlmModel
from docling.models.rapid_ocr_model import RapidOcrModel
from docling.models.table_structure_model import TableStructureModel
from docling.models.utils.hf_model_download import download_hf_model
@@ -41,6 +42,7 @@ def download_models(
with_smoldocling: bool = False,
with_smoldocling_mlx: bool = False,
with_granite_vision: bool = False,
with_rapidocr: bool = True,
with_easyocr: bool = True,
):
if output_dir is None:
@@ -135,6 +137,16 @@ def download_models(
progress=progress,
)
if with_rapidocr:
for backend in ("torch", "onnxruntime"):
_log.info(f"Downloading rapidocr {backend} models...")
RapidOcrModel.download_models(
backend=backend,
local_dir=output_dir / RapidOcrModel._model_repo_folder,
force=force,
progress=progress,
)
if with_easyocr:
_log.info("Downloading easyocr models...")
EasyOcrModel.download_models(

View File

@@ -52,7 +52,8 @@ dependencies = [
'pydantic-settings (>=2.3.0,<3.0.0)',
'huggingface_hub (>=0.23,<1)',
'requests (>=2.32.2,<3.0.0)',
'easyocr (>=1.7,<2.0)',
'ocrmac (>=1.0.0,<2.0.0) ; sys_platform == "darwin"',
'rapidocr (>=3.3,<4.0.0) ; python_version < "3.14"',
'certifi (>=2024.7.4)',
'rtree (>=1.3.0,<2.0.0)',
'typer (>=0.12.5,<0.20.0)',
@@ -88,6 +89,7 @@ docling = "docling.cli.main:app"
docling-tools = "docling.cli.tools:app"
[project.optional-dependencies]
easyocr = ['easyocr (>=1.7,<2.0)']
tesserocr = ['tesserocr (>=2.7.1,<3.0.0)']
ocrmac = ['ocrmac (>=1.0.0,<2.0.0) ; sys_platform == "darwin"']
vlm = [
@@ -100,7 +102,6 @@ vlm = [
rapidocr = [
'rapidocr (>=3.3,<4.0.0) ; python_version < "3.14"',
'onnxruntime (>=1.7.0,<2.0.0)',
"modelscope>=1.29.0",
# 'onnxruntime (>=1.7.0,<2.0.0) ; python_version >= "3.10"',
# 'onnxruntime (>=1.7.0,<1.20.0) ; python_version < "3.10"',
]
@@ -143,6 +144,7 @@ examples = [
"langchain-huggingface>=0.0.3",
"langchain-milvus~=0.1",
"langchain-text-splitters~=0.2",
"modelscope>=1.29.0",
]
constraints = [
'onnxruntime (>=1.7.0,<2.0.0) ; python_version >= "3.10"',
@@ -252,6 +254,7 @@ module = [
"docling_ibm_models.*",
"easyocr.*",
"ocrmac.*",
"onnxruntime.*",
"mlx_vlm.*",
"lxml.*",
"huggingface_hub.*",

21
uv.lock generated
View File

@@ -1093,11 +1093,11 @@ dependencies = [
{ name = "docling-core", extra = ["chunking"] },
{ name = "docling-ibm-models" },
{ name = "docling-parse" },
{ name = "easyocr" },
{ name = "filetype" },
{ name = "huggingface-hub" },
{ name = "lxml" },
{ name = "marko" },
{ name = "ocrmac", marker = "sys_platform == 'darwin'" },
{ name = "openpyxl" },
{ name = "pandas" },
{ name = "pillow" },
@@ -1109,6 +1109,7 @@ dependencies = [
{ name = "pypdfium2" },
{ name = "python-docx" },
{ name = "python-pptx" },
{ name = "rapidocr", marker = "python_full_version < '3.14'" },
{ name = "requests" },
{ name = "rtree" },
{ name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1122,11 +1123,13 @@ dependencies = [
asr = [
{ name = "openai-whisper" },
]
easyocr = [
{ name = "easyocr" },
]
ocrmac = [
{ name = "ocrmac", marker = "sys_platform == 'darwin'" },
]
rapidocr = [
{ name = "modelscope" },
{ name = "onnxruntime", version = "1.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "onnxruntime", version = "1.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "rapidocr", marker = "python_full_version < '3.14'" },
@@ -1181,6 +1184,7 @@ examples = [
{ name = "langchain-huggingface" },
{ name = "langchain-milvus" },
{ name = "langchain-text-splitters" },
{ name = "modelscope" },
{ name = "python-dotenv" },
]
@@ -1193,13 +1197,13 @@ requires-dist = [
{ name = "docling-core", extras = ["chunking"], specifier = ">=2.48.2,<3.0.0" },
{ name = "docling-ibm-models", specifier = ">=3.9.1,<4" },
{ name = "docling-parse", specifier = ">=4.4.0,<5.0.0" },
{ name = "easyocr", specifier = ">=1.7,<2.0" },
{ name = "easyocr", marker = "extra == 'easyocr'", specifier = ">=1.7,<2.0" },
{ name = "filetype", specifier = ">=1.2.0,<2.0.0" },
{ name = "huggingface-hub", specifier = ">=0.23,<1" },
{ name = "lxml", specifier = ">=4.0.0,<6.0.0" },
{ name = "marko", specifier = ">=2.1.2,<3.0.0" },
{ name = "mlx-vlm", marker = "python_full_version >= '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'vlm'", specifier = ">=0.3.0,<1.0.0" },
{ name = "modelscope", marker = "extra == 'rapidocr'", specifier = ">=1.29.0" },
{ name = "ocrmac", marker = "sys_platform == 'darwin'", specifier = ">=1.0.0,<2.0.0" },
{ name = "ocrmac", marker = "sys_platform == 'darwin' and extra == 'ocrmac'", specifier = ">=1.0.0,<2.0.0" },
{ name = "onnxruntime", marker = "extra == 'rapidocr'", specifier = ">=1.7.0,<2.0.0" },
{ name = "openai-whisper", marker = "extra == 'asr'", specifier = ">=20250625" },
@@ -1215,6 +1219,7 @@ requires-dist = [
{ name = "python-docx", specifier = ">=1.1.2,<2.0.0" },
{ name = "python-pptx", specifier = ">=1.0.2,<2.0.0" },
{ name = "qwen-vl-utils", marker = "extra == 'vlm'", specifier = ">=0.0.11" },
{ name = "rapidocr", marker = "python_full_version < '3.14'", specifier = ">=3.3,<4.0.0" },
{ name = "rapidocr", marker = "python_full_version < '3.14' and extra == 'rapidocr'", specifier = ">=3.3,<4.0.0" },
{ name = "requests", specifier = ">=2.32.2,<3.0.0" },
{ name = "rtree", specifier = ">=1.3.0,<2.0.0" },
@@ -1225,7 +1230,7 @@ requires-dist = [
{ name = "typer", specifier = ">=0.12.5,<0.20.0" },
{ name = "vllm", marker = "python_full_version >= '3.10' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'vlm'", specifier = ">=0.10.0,<1.0.0" },
]
provides-extras = ["tesserocr", "ocrmac", "vlm", "rapidocr", "asr"]
provides-extras = ["easyocr", "tesserocr", "ocrmac", "vlm", "rapidocr", "asr"]
[package.metadata.requires-dev]
constraints = [
@@ -1265,6 +1270,7 @@ examples = [
{ name = "langchain-huggingface", specifier = ">=0.0.3" },
{ name = "langchain-milvus", specifier = "~=0.1" },
{ name = "langchain-text-splitters", specifier = "~=0.2" },
{ name = "modelscope", specifier = ">=1.29.0" },
{ name = "python-dotenv", specifier = "~=1.0" },
]
@@ -5039,6 +5045,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/8a/b35a615ae6f04550d696bb179c414538b3b477999435fdd4ad75b76139e4/pybase64-1.4.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a370dea7b1cee2a36a4d5445d4e09cc243816c5bc8def61f602db5a6f5438e52", size = 54320, upload-time = "2025-07-27T13:03:27.495Z" },
{ url = "https://files.pythonhosted.org/packages/d3/a9/8bd4f9bcc53689f1b457ecefed1eaa080e4949d65a62c31a38b7253d5226/pybase64-1.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9aa4de83f02e462a6f4e066811c71d6af31b52d7484de635582d0e3ec3d6cc3e", size = 56482, upload-time = "2025-07-27T13:03:28.942Z" },
{ url = "https://files.pythonhosted.org/packages/75/e5/4a7735b54a1191f61c3f5c2952212c85c2d6b06eb5fb3671c7603395f70c/pybase64-1.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83a1c2f9ed00fee8f064d548c8654a480741131f280e5750bb32475b7ec8ee38", size = 70959, upload-time = "2025-07-27T13:03:30.171Z" },
{ url = "https://files.pythonhosted.org/packages/f4/56/5337f27a8b8d2d6693f46f7b36bae47895e5820bfa259b0072574a4e1057/pybase64-1.4.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:0f331aa59549de21f690b6ccc79360ffed1155c3cfbc852eb5c097c0b8565a2b", size = 33888, upload-time = "2025-07-27T13:03:35.698Z" },
{ url = "https://files.pythonhosted.org/packages/e3/ff/470768f0fe6de0aa302a8cb1bdf2f9f5cffc3f69e60466153be68bc953aa/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:69d3f0445b0faeef7bb7f93bf8c18d850785e2a77f12835f49e524cc54af04e7", size = 30914, upload-time = "2025-07-27T13:03:38.475Z" },
{ url = "https://files.pythonhosted.org/packages/75/6b/d328736662665e0892409dc410353ebef175b1be5eb6bab1dad579efa6df/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2372b257b1f4dd512f317fb27e77d313afd137334de64c87de8374027aacd88a", size = 31380, upload-time = "2025-07-27T13:03:39.7Z" },
{ url = "https://files.pythonhosted.org/packages/ca/96/7ff718f87c67f4147c181b73d0928897cefa17dc75d7abc6e37730d5908f/pybase64-1.4.2-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fb794502b4b1ec91c4ca5d283ae71aef65e3de7721057bd9e2b3ec79f7a62d7d", size = 38230, upload-time = "2025-07-27T13:03:41.637Z" },
{ url = "https://files.pythonhosted.org/packages/71/ab/db4dbdfccb9ca874d6ce34a0784761471885d96730de85cee3d300381529/pybase64-1.4.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d377d48acf53abf4b926c2a7a24a19deb092f366a04ffd856bf4b3aa330b025d", size = 71608, upload-time = "2025-07-27T13:03:47.01Z" },
{ url = "https://files.pythonhosted.org/packages/f2/58/7f2cef1ceccc682088958448d56727369de83fa6b29148478f4d2acd107a/pybase64-1.4.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:ab9cdb6a8176a5cb967f53e6ad60e40c83caaa1ae31c5e1b29e5c8f507f17538", size = 56413, upload-time = "2025-07-27T13:03:49.908Z" },
@@ -5060,6 +5069,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/95/f0/c392c4ac8ccb7a34b28377c21faa2395313e3c676d76c382642e19a20703/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad59362fc267bf15498a318c9e076686e4beeb0dfe09b457fabbc2b32468b97a", size = 58103, upload-time = "2025-07-27T13:04:29.996Z" },
{ url = "https://files.pythonhosted.org/packages/32/30/00ab21316e7df8f526aa3e3dc06f74de6711d51c65b020575d0105a025b2/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:01593bd064e7dcd6c86d04e94e44acfe364049500c20ac68ca1e708fbb2ca970", size = 60779, upload-time = "2025-07-27T13:04:31.549Z" },
{ url = "https://files.pythonhosted.org/packages/a6/65/114ca81839b1805ce4a2b7d58bc16e95634734a2059991f6382fc71caf3e/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5b81547ad8ea271c79fdf10da89a1e9313cb15edcba2a17adf8871735e9c02a0", size = 74684, upload-time = "2025-07-27T13:04:32.976Z" },
{ url = "https://files.pythonhosted.org/packages/99/bf/00a87d951473ce96c8c08af22b6983e681bfabdb78dd2dcf7ee58eac0932/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4157ad277a32cf4f02a975dffc62a3c67d73dfa4609b2c1978ef47e722b18b8e", size = 30924, upload-time = "2025-07-27T13:04:39.189Z" },
{ url = "https://files.pythonhosted.org/packages/ae/43/dee58c9d60e60e6fb32dc6da722d84592e22f13c277297eb4ce6baf99a99/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e113267dc349cf624eb4f4fbf53fd77835e1aa048ac6877399af426aab435757", size = 31390, upload-time = "2025-07-27T13:04:40.995Z" },
{ url = "https://files.pythonhosted.org/packages/e1/11/b28906fc2e330b8b1ab4bc845a7bef808b8506734e90ed79c6062b095112/pybase64-1.4.2-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:cea5aaf218fd9c5c23afacfe86fd4464dfedc1a0316dd3b5b4075b068cc67df0", size = 38212, upload-time = "2025-07-27T13:04:42.729Z" },
{ url = "https://files.pythonhosted.org/packages/e4/2e/851eb51284b97354ee5dfa1309624ab90920696e91a33cd85b13d20cc5c1/pybase64-1.4.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a3e54dcf0d0305ec88473c9d0009f698cabf86f88a8a10090efeff2879c421bb", size = 71674, upload-time = "2025-07-27T13:04:49.294Z" },
{ url = "https://files.pythonhosted.org/packages/a4/8e/3479266bc0e65f6cc48b3938d4a83bff045330649869d950a378f2ddece0/pybase64-1.4.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:753da25d4fd20be7bda2746f545935773beea12d5cb5ec56ec2d2960796477b1", size = 56461, upload-time = "2025-07-27T13:04:52.37Z" },