1414 docling_rs.download_models() # once; idempotent, skips present files
1515
1616``DocumentConverter`` calls :func:`ensure_env` automatically, so after the
17- one-time download no configuration is needed at all.
17+ one-time download no configuration is needed at all. Local assets outrank the
18+ cache: when a matching ``models/`` / ``.pdfium/`` asset exists in the working
19+ directory (e.g. a repo checkout with its own exports), the env var is left
20+ unset and the native pipeline resolves the local path itself, exactly like
21+ the Rust CLI. Re-published release assets are picked up with
22+ ``download_models(force=True)`` — the cache has no version stamp.
1823"""
1924
2025from __future__ import annotations
4348_OPTIONAL = {
4449 "layout_heron_int8.onnx" : "models/layout_heron_int8.onnx" ,
4550 "decoder_int8.onnx" : "models/tableformer/decoder_int8.onnx" ,
51+ # The #97 hoisted-KV TableFormer decoder — byte-exact vs the legacy graph
52+ # and the fastest variant on every machine measured; ensure_env prefers it.
53+ "decoder_kv.onnx" : "models/tableformer/decoder_kv.onnx" ,
54+ "decoder_kv.onnx.data" : "models/tableformer/decoder_kv.onnx.data" ,
55+ "decoder_kv_int8.onnx" : "models/tableformer/decoder_kv_int8.onnx" ,
56+ # DocumentFigureClassifier-v2.5 (~17 MB) for do_picture_classification;
57+ # missing file just skips the enrichment with a one-time warning.
58+ "picture_classifier.onnx" : "models/picture_classifier.onnx" ,
4659 "encoder.onnx.data" : "models/tableformer/encoder.onnx.data" ,
4760 "decoder.onnx.data" : "models/tableformer/decoder.onnx.data" ,
4861 "bbox.onnx.data" : "models/tableformer/bbox.onnx.data" ,
5164 "chunk_tokenizer.json" : "models/chunk/tokenizer.json" ,
5265}
5366
67+ # CodeFormula (do_code_enrichment / do_formula_enrichment) — the int8 decoder
68+ # (~165 MB) makes the ~655 MB fp32 decoder unnecessary (same rule as
69+ # download_dependencies.sh), so the fp32 graph is fetched only when the int8
70+ # variant isn't hosted.
71+ _ENRICH = {
72+ "cf_vision.onnx" : "models/code_formula/vision.onnx" ,
73+ "cf_embed.onnx" : "models/code_formula/embed.onnx" ,
74+ "cf_decoder_kv_int8.onnx" : "models/code_formula/decoder_kv_int8.onnx" ,
75+ "cf_tokenizer.json" : "models/code_formula/tokenizer.json" ,
76+ }
77+ _ENRICH_FP32_DECODER = ("cf_decoder_kv.onnx" , "models/code_formula/decoder_kv.onnx" )
78+
5479# Straight-from-upstream fallback for assets older release tags don't host:
5580# cache path -> upstream URL.
5681_FALLBACK_URLS = {
5782 "models/chunk/tokenizer.json" : (
5883 "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json"
5984 ),
85+ "models/picture_classifier.onnx" : (
86+ "https://huggingface.co/docling-project/DocumentFigureClassifier-v2.5/resolve/main/model.onnx"
87+ ),
6088}
6189# pdfium rasterizer (Linux x64, matching what the release hosts).
6290_PDFIUM = {"libpdfium.so" : ".pdfium/lib/libpdfium.so" }
@@ -69,8 +97,8 @@ def cache_dir() -> Path:
6997 return Path (os .environ .get ("XDG_CACHE_HOME" , Path .home () / ".cache" )) / "docling.rs"
7098
7199
72- def _fetch (url : str , dest : Path , optional : bool , progress : bool ) -> bool :
73- if dest .exists ():
100+ def _fetch (url : str , dest : Path , optional : bool , progress : bool , force : bool = False ) -> bool :
101+ if dest .exists () and not force :
74102 return True
75103 dest .parent .mkdir (parents = True , exist_ok = True )
76104 tmp = dest .with_suffix (dest .suffix + ".download" )
@@ -89,55 +117,117 @@ def _fetch(url: str, dest: Path, optional: bool, progress: bool) -> bool:
89117 raise
90118
91119
92- def download_models (dest : "str | Path | None" = None , progress : bool = True ) -> Path :
120+ def download_models (
121+ dest : "str | Path | None" = None , progress : bool = True , force : bool = False
122+ ) -> Path :
93123 """Fetch the PDF/image pipeline's models + pdfium into the cache (idempotent).
94124
95125 Returns the cache root. Pass ``dest`` to use a custom directory (also set
96126 it as ``$DOCLING_RS_CACHE_DIR`` at runtime, or pass the same value as
97- ``DocumentConverter(artifacts_path=...)``).
127+ ``DocumentConverter(artifacts_path=...)``). Pass ``force=True`` to
128+ re-download files that are already cached — the cache has no version
129+ stamp, so this is how a stale cache picks up re-published model assets
130+ (e.g. the dynamic-batch layout graph or the hoisted-KV TableFormer
131+ decoder).
98132 """
99133 root = Path (dest ) if dest else cache_dir ()
100134 if progress :
101135 print (f"docling.rs: fetching models to { root } " , file = sys .stderr , flush = True )
102136 for name , rel in {** _REQUIRED , ** _PDFIUM }.items ():
103- _fetch (f"{ BASE_URL } /{ name } " , root / rel , optional = False , progress = progress )
104- for name , rel in _OPTIONAL .items ():
105- if not _fetch (f"{ BASE_URL } /{ name } " , root / rel , optional = True , progress = progress ):
137+ _fetch (f"{ BASE_URL } /{ name } " , root / rel , optional = False , progress = progress , force = force )
138+ for name , rel in {** _OPTIONAL , ** _ENRICH }.items ():
139+ if not _fetch (
140+ f"{ BASE_URL } /{ name } " , root / rel , optional = True , progress = progress , force = force
141+ ):
106142 if fallback := _FALLBACK_URLS .get (rel ):
107- _fetch (fallback , root / rel , optional = True , progress = progress )
143+ _fetch (fallback , root / rel , optional = True , progress = progress , force = force )
144+ # The huge fp32 CodeFormula decoder only matters when its int8 variant
145+ # isn't hosted (or DOCLING_RS_FP32 users fetch it here as the fallback).
146+ name , rel = _ENRICH_FP32_DECODER
147+ if not (root / _ENRICH ["cf_decoder_kv_int8.onnx" ]).exists ():
148+ _fetch (f"{ BASE_URL } /{ name } " , root / rel , optional = True , progress = progress , force = force )
108149 return root
109150
110151
111- def _setdefault_if_exists (var : str , path : Path ) -> None :
112- if var not in os .environ and path .exists ():
113- os .environ [var ] = str (path )
152+ def _point_at (var : str , local : "list[str]" , cached : Path ) -> None :
153+ """Set ``var`` to ``cached`` unless configuration already exists.
154+
155+ Two things outrank the cache: an env var the caller already set, and a
156+ matching asset in the working directory (any of the ``local`` relative
157+ paths) — the native pipeline resolves those CWD paths itself when the env
158+ var stays unset, exactly like the Rust CLI run from a checkout. The env
159+ is also left untouched when ``cached`` doesn't exist."""
160+ if var in os .environ :
161+ return
162+ if any (Path (rel ).exists () for rel in local ):
163+ return
164+ if cached .exists ():
165+ os .environ [var ] = str (cached )
114166
115167
116168def ensure_env (dest : "str | Path | None" = None ) -> Path :
117169 """Point the native pipeline at the cached assets via the ``DOCLING_*`` /
118- ``PDFIUM_*`` env vars (only filling ones that are not already set, so
119- explicit configuration always wins). Prefers the INT8 models when present,
120- matching the Rust pipeline's default; ``DOCLING_RS_FP32=1`` opts out.
121- Safe to call when nothing is downloaded yet — missing files simply leave
122- the env untouched (and the converter will fail with its usual clear
170+ ``PDFIUM_*`` env vars. Local assets win: a variable is only filled when it
171+ is not already set AND no matching ``models/`` / ``.pdfium/`` asset exists
172+ in the working directory (the native code resolves those itself, so a repo
173+ checkout keeps using its own exports). Prefers the INT8 models when
174+ present, matching the Rust pipeline's default; ``DOCLING_RS_FP32=1`` opts
175+ out. Safe to call when nothing is downloaded yet — missing files simply
176+ leave the env untouched (and the converter will fail with its usual clear
123177 "model not found" message)."""
124178 root = Path (dest ) if dest else cache_dir ()
125179 fp32 = os .environ .get ("DOCLING_RS_FP32" , "0" ) not in ("" , "0" )
126180 m = root / "models"
127181
182+ layout_chain = ["models/layout_heron.onnx" ]
183+ if not fp32 :
184+ layout_chain .insert (0 , "models/layout_heron_int8.onnx" )
128185 layout = m / "layout_heron_int8.onnx"
129186 if fp32 or not layout .exists ():
130187 layout = m / "layout_heron.onnx"
131- _setdefault_if_exists ("DOCLING_LAYOUT_ONNX" , layout )
132-
133- decoder = m / "tableformer/decoder_int8.onnx"
134- if fp32 or not decoder .exists ():
135- decoder = m / "tableformer/decoder.onnx"
136- _setdefault_if_exists ("DOCLING_TABLEFORMER_DECODER" , decoder )
137-
138- _setdefault_if_exists ("DOCLING_OCR_REC_ONNX" , m / "ocr_rec.onnx" )
139- _setdefault_if_exists ("DOCLING_OCR_DICT" , m / "ppocr_keys_v1.txt" )
140- _setdefault_if_exists ("DOCLING_TABLEFORMER_ENCODER" , m / "tableformer/encoder.onnx" )
141- _setdefault_if_exists ("DOCLING_TABLEFORMER_BBOX" , m / "tableformer/bbox.onnx" )
142- _setdefault_if_exists ("PDFIUM_DYNAMIC_LIB_PATH" , root / ".pdfium/lib" )
188+ _point_at ("DOCLING_LAYOUT_ONNX" , layout_chain , layout )
189+
190+ # TableFormer decoder preference, mirroring the Rust pipeline's default
191+ # chain (tableformer.rs): the #97 hoisted-KV graph ranks ahead of the
192+ # legacy layer-output-cache graph within each precision, and decoder_kv
193+ # (fp32) ranks above the quantized *legacy* decoder — it is faster on
194+ # every machine measured and byte-exact.
195+ if fp32 :
196+ chain = ["tableformer/decoder_kv.onnx" , "tableformer/decoder.onnx" ]
197+ else :
198+ chain = [
199+ "tableformer/decoder_kv_int8.onnx" ,
200+ "tableformer/decoder_kv.onnx" ,
201+ "tableformer/decoder_int8.onnx" ,
202+ "tableformer/decoder.onnx" ,
203+ ]
204+ decoder = next ((p for rel in chain if (p := m / rel ).exists ()), m / "tableformer/decoder.onnx" )
205+ _point_at ("DOCLING_TABLEFORMER_DECODER" , [f"models/{ rel } " for rel in chain ], decoder )
206+
207+ classifier_chain = ["models/picture_classifier.onnx" ]
208+ if not fp32 :
209+ classifier_chain .insert (0 , "models/picture_classifier_int8.onnx" )
210+ classifier = m / "picture_classifier_int8.onnx"
211+ if fp32 or not classifier .exists ():
212+ classifier = m / "picture_classifier.onnx"
213+ _point_at ("DOCLING_PICTURE_CLASSIFIER_ONNX" , classifier_chain , classifier )
214+
215+ _point_at ("DOCLING_OCR_REC_ONNX" , ["models/ocr_rec.onnx" ], m / "ocr_rec.onnx" )
216+ _point_at ("DOCLING_OCR_DICT" , ["models/ppocr_keys_v1.txt" ], m / "ppocr_keys_v1.txt" )
217+ _point_at (
218+ "DOCLING_TABLEFORMER_ENCODER" ,
219+ ["models/tableformer/encoder.onnx" ],
220+ m / "tableformer/encoder.onnx" ,
221+ )
222+ _point_at (
223+ "DOCLING_TABLEFORMER_BBOX" ,
224+ ["models/tableformer/bbox.onnx" ],
225+ m / "tableformer/bbox.onnx" ,
226+ )
227+ _point_at (
228+ "DOCLING_CODE_FORMULA_DIR" ,
229+ ["models/code_formula" ],
230+ m / "code_formula" ,
231+ )
232+ _point_at ("PDFIUM_DYNAMIC_LIB_PATH" , [".pdfium/lib" ], root / ".pdfium/lib" )
143233 return root
0 commit comments