BabelDOC 0.7.0__cp310-abi3-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. babeldoc/__init__.py +1 -0
  2. babeldoc/_rust_il.pyd +0 -0
  3. babeldoc/assets/assets.py +610 -0
  4. babeldoc/assets/embedding_assets_metadata.py +1450 -0
  5. babeldoc/asynchronize/__init__.py +51 -0
  6. babeldoc/babeldoc_exception/BabelDOCException.py +19 -0
  7. babeldoc/babeldoc_exception/__init__.py +0 -0
  8. babeldoc/const.py +95 -0
  9. babeldoc/docvision/README.md +0 -0
  10. babeldoc/docvision/__init__.py +0 -0
  11. babeldoc/docvision/base_doclayout.py +68 -0
  12. babeldoc/docvision/doclayout.py +284 -0
  13. babeldoc/docvision/rpc_doclayout.py +329 -0
  14. babeldoc/docvision/rpc_doclayout2.py +337 -0
  15. babeldoc/docvision/rpc_doclayout3.py +330 -0
  16. babeldoc/docvision/rpc_doclayout4.py +337 -0
  17. babeldoc/docvision/rpc_doclayout5.py +328 -0
  18. babeldoc/docvision/rpc_doclayout6.py +665 -0
  19. babeldoc/docvision/rpc_doclayout7.py +353 -0
  20. babeldoc/docvision/rpc_doclayout8.py +365 -0
  21. babeldoc/docvision/table_detection/rapidocr.py +41 -0
  22. babeldoc/format/__init__.py +0 -0
  23. babeldoc/format/pdf/__init__.py +0 -0
  24. babeldoc/format/pdf/babelpdf/cidfont.py +67 -0
  25. babeldoc/format/pdf/babelpdf/cmap.py +147 -0
  26. babeldoc/format/pdf/babelpdf/encoding.py +1307 -0
  27. babeldoc/format/pdf/document_il/__init__.py +67 -0
  28. babeldoc/format/pdf/document_il/backend/__init__.py +0 -0
  29. babeldoc/format/pdf/document_il/backend/pdf_creater.py +700 -0
  30. babeldoc/format/pdf/document_il/frontend/__init__.py +0 -0
  31. babeldoc/format/pdf/document_il/frontend/il_creater_active_support.py +566 -0
  32. babeldoc/format/pdf/document_il/il_version_1.py +1389 -0
  33. babeldoc/format/pdf/document_il/il_version_1.rnc +244 -0
  34. babeldoc/format/pdf/document_il/il_version_1.rng +670 -0
  35. babeldoc/format/pdf/document_il/il_version_1.xsd +383 -0
  36. babeldoc/format/pdf/document_il/midend/__init__.py +0 -0
  37. babeldoc/format/pdf/document_il/utils/__init__.py +0 -0
  38. babeldoc/format/pdf/document_il/utils/extract_char.py +809 -0
  39. babeldoc/format/pdf/document_il/utils/fontmap.py +315 -0
  40. babeldoc/format/pdf/document_il/utils/formular_helper.py +335 -0
  41. babeldoc/format/pdf/document_il/utils/layout_helper.py +15 -0
  42. babeldoc/format/pdf/document_il/utils/mupdf_helper.py +77 -0
  43. babeldoc/format/pdf/document_il/utils/raster_geometry.py +256 -0
  44. babeldoc/format/pdf/document_il/xml_converter.py +61 -0
  45. babeldoc/format/pdf/high_level.py +1274 -0
  46. babeldoc/format/pdf/parser_errors.py +14 -0
  47. babeldoc/format/pdf/parser_resource.py +50 -0
  48. babeldoc/format/pdf/result_merger.py +194 -0
  49. babeldoc/format/pdf/split_manager.py +67 -0
  50. babeldoc/format/pdf/translation_config.py +643 -0
  51. babeldoc/glossary.py +243 -0
  52. babeldoc/main.py +973 -0
  53. babeldoc/progress_monitor.py +315 -0
  54. babeldoc/rust_callback_bridge.py +78 -0
  55. babeldoc/rust_debug_service.py +122 -0
  56. babeldoc/rust_layout_service.py +242 -0
  57. babeldoc/rust_parser_service.py +101 -0
  58. babeldoc/rust_run_context.py +126 -0
  59. babeldoc/rust_scan_service.py +103 -0
  60. babeldoc/rust_translation_service.py +128 -0
  61. babeldoc/rust_writer_service.py +135 -0
  62. babeldoc/tools/executor/__init__.py +0 -0
  63. babeldoc/tools/executor/__main__.py +4 -0
  64. babeldoc/tools/executor/babeldoc_adapter.py +475 -0
  65. babeldoc/tools/executor/protocol.py +35 -0
  66. babeldoc/tools/executor/runner.py +417 -0
  67. babeldoc/tools/executor/server.py +490 -0
  68. babeldoc/tools/executor/state.py +319 -0
  69. babeldoc/tools/executor/translator.py +164 -0
  70. babeldoc/tools/executor/watermark_transform.py +175 -0
  71. babeldoc/tools/executor/workroot.py +61 -0
  72. babeldoc/tools/generate_cmap_metadata.py +73 -0
  73. babeldoc/tools/generate_font_metadata.py +144 -0
  74. babeldoc/tools/italic_assistance.py +294 -0
  75. babeldoc/tools/italic_recognize_tool.py +78 -0
  76. babeldoc/translator/__init__.py +0 -0
  77. babeldoc/translator/cache.py +199 -0
  78. babeldoc/translator/translator.py +377 -0
  79. babeldoc/utils/__init__.py +0 -0
  80. babeldoc/utils/atomic_integer.py +26 -0
  81. babeldoc/utils/memory.py +251 -0
  82. babeldoc/utils/priority_thread_pool_executor.py +269 -0
  83. babeldoc-0.7.0.dist-info/METADATA +502 -0
  84. babeldoc-0.7.0.dist-info/RECORD +88 -0
  85. babeldoc-0.7.0.dist-info/WHEEL +4 -0
  86. babeldoc-0.7.0.dist-info/entry_points.txt +2 -0
  87. babeldoc-0.7.0.dist-info/licenses/LICENSE +661 -0
  88. babeldoc-0.7.0.dist-info/sboms/babeldoc-p2-bridge.cyclonedx.json +4584 -0
babeldoc/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.7.0"
babeldoc/_rust_il.pyd ADDED
Binary file
@@ -0,0 +1,610 @@
1
+ import asyncio
2
+ import hashlib
3
+ import json
4
+ import logging
5
+ import threading
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+ from babeldoc.assets import embedding_assets_metadata
11
+ from babeldoc.assets.embedding_assets_metadata import CMAP_METADATA
12
+ from babeldoc.assets.embedding_assets_metadata import CMAP_URL_BY_UPSTREAM
13
+ from babeldoc.assets.embedding_assets_metadata import DOC_LAYOUT_ONNX_MODEL_URL
14
+ from babeldoc.assets.embedding_assets_metadata import (
15
+ DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256,
16
+ )
17
+ from babeldoc.assets.embedding_assets_metadata import EMBEDDING_FONT_METADATA
18
+ from babeldoc.assets.embedding_assets_metadata import FONT_METADATA_URL
19
+ from babeldoc.assets.embedding_assets_metadata import FONT_URL_BY_UPSTREAM
20
+ from babeldoc.assets.embedding_assets_metadata import TIKTOKEN_CACHES
21
+ from babeldoc.const import get_cache_file_path
22
+ from tenacity import retry
23
+ from tenacity import stop_after_attempt
24
+ from tenacity import wait_exponential
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ _FASTEST_FONT_UPSTREAM_LOCK = asyncio.Lock()
30
+ _FASTEST_FONT_UPSTREAM: str | None = None
31
+ _FASTEST_FONT_METADATA: dict | None = None
32
+
33
+
34
+ class ResultContainer:
35
+ def __init__(self):
36
+ self.result = None
37
+ self.exception: BaseException | None = None
38
+
39
+ def set_result(self, result):
40
+ self.result = result
41
+
42
+ def set_exception(self, exc: BaseException):
43
+ self.exception = exc
44
+
45
+
46
+ def run_in_another_thread(coro):
47
+ result_container = ResultContainer()
48
+
49
+ def _wrapper():
50
+ try:
51
+ result_container.set_result(asyncio.run(coro))
52
+ except BaseException as exc: # noqa: BLE001
53
+ result_container.set_exception(exc)
54
+
55
+ thread = threading.Thread(target=_wrapper)
56
+ thread.start()
57
+ thread.join()
58
+ if result_container.exception is not None:
59
+ msg = (
60
+ "asset coroutine failed: "
61
+ f"{type(result_container.exception).__name__}: {result_container.exception}"
62
+ )
63
+ raise RuntimeError(msg) from result_container.exception
64
+ return result_container.result
65
+
66
+
67
+ def run_coro(coro):
68
+ return run_in_another_thread(coro)
69
+
70
+
71
+ def _retry_if_not_cancelled_and_failed(retry_state):
72
+ """Only retry if the exception is not CancelledError and the attempt failed."""
73
+ if retry_state.outcome.failed:
74
+ exception = retry_state.outcome.exception()
75
+ # Don't retry on CancelledError
76
+ if isinstance(exception, asyncio.CancelledError):
77
+ logger.debug("Operation was cancelled, not retrying")
78
+ return False
79
+ # Retry on network related errors
80
+ if isinstance(
81
+ exception, httpx.HTTPError | ConnectionError | ValueError | TimeoutError
82
+ ):
83
+ logger.warning(f"Network error occurred: {exception}, will retry")
84
+ return True
85
+ # Don't retry on success
86
+ return False
87
+
88
+
89
+ def verify_file(path: Path, sha3_256: str):
90
+ if not path.exists():
91
+ return False
92
+ hash_ = hashlib.sha3_256()
93
+ with path.open("rb") as f:
94
+ while True:
95
+ chunk = f.read(1024 * 1024)
96
+ if not chunk:
97
+ break
98
+ hash_.update(chunk)
99
+ return hash_.hexdigest() == sha3_256
100
+
101
+
102
+ @retry(
103
+ retry=_retry_if_not_cancelled_and_failed,
104
+ stop=stop_after_attempt(3),
105
+ wait=wait_exponential(multiplier=1, min=1, max=15),
106
+ before_sleep=lambda retry_state: logger.warning(
107
+ f"Download file failed, retrying in {retry_state.next_action.sleep} seconds... "
108
+ f"(Attempt {retry_state.attempt_number}/3)"
109
+ ),
110
+ )
111
+ async def download_file(
112
+ client: httpx.AsyncClient | None = None,
113
+ url: str = None,
114
+ path: Path = None,
115
+ sha3_256: str = None,
116
+ ):
117
+ if client is None:
118
+ async with httpx.AsyncClient() as client:
119
+ response = await client.get(url, follow_redirects=True)
120
+ else:
121
+ response = await client.get(url, follow_redirects=True)
122
+
123
+ response.raise_for_status()
124
+ with path.open("wb") as f:
125
+ f.write(response.content)
126
+ if not verify_file(path, sha3_256):
127
+ path.unlink(missing_ok=True)
128
+ raise ValueError(f"File {path} is corrupted")
129
+
130
+
131
+ @retry(
132
+ retry=_retry_if_not_cancelled_and_failed,
133
+ stop=stop_after_attempt(3),
134
+ wait=wait_exponential(multiplier=1, min=1, max=15),
135
+ before_sleep=lambda retry_state: logger.warning(
136
+ f"Get font metadata failed, retrying in {retry_state.next_action.sleep} seconds... "
137
+ f"(Attempt {retry_state.attempt_number}/3)"
138
+ ),
139
+ )
140
+ async def get_font_metadata(
141
+ client: httpx.AsyncClient | None = None, upstream: str = None
142
+ ):
143
+ if upstream not in FONT_METADATA_URL:
144
+ logger.critical(f"Invalid upstream: {upstream}")
145
+ exit(1)
146
+
147
+ if client is None:
148
+ async with httpx.AsyncClient() as client:
149
+ response = await client.get(
150
+ FONT_METADATA_URL[upstream], follow_redirects=True
151
+ )
152
+ else:
153
+ response = await client.get(FONT_METADATA_URL[upstream], follow_redirects=True)
154
+
155
+ response.raise_for_status()
156
+ logger.debug(f"Get font metadata from {upstream} success")
157
+ return upstream, response.json()
158
+
159
+
160
+ async def _get_fastest_upstream_for_font_internal(
161
+ client: httpx.AsyncClient | None = None, exclude_upstream: list[str] | None = None
162
+ ) -> tuple[str | None, dict | None]:
163
+ """Find the fastest upstream for font metadata without using cached result."""
164
+ tasks: list[asyncio.Task[tuple[str, dict]]] = []
165
+ for upstream in FONT_METADATA_URL:
166
+ if exclude_upstream and upstream in exclude_upstream:
167
+ continue
168
+ tasks.append(asyncio.create_task(get_font_metadata(client, upstream)))
169
+ for future in asyncio.as_completed(tasks):
170
+ try:
171
+ result = await future
172
+ for task in tasks:
173
+ if not task.done():
174
+ task.cancel()
175
+ return result
176
+ except Exception as e:
177
+ logger.exception(f"Error getting font metadata: {e}")
178
+ logger.error("All upstreams failed")
179
+ return None, None
180
+
181
+
182
+ async def get_fastest_upstream_for_font(
183
+ client: httpx.AsyncClient | None = None, exclude_upstream: list[str] | None = None
184
+ ) -> tuple[str | None, dict | None]:
185
+ """Get the fastest upstream for font metadata with cached result.
186
+
187
+ The cached upstream is only used when exclude_upstream is None.
188
+ """
189
+ global _FASTEST_FONT_UPSTREAM, _FASTEST_FONT_METADATA
190
+
191
+ if exclude_upstream is None and _FASTEST_FONT_UPSTREAM is not None:
192
+ return _FASTEST_FONT_UPSTREAM, _FASTEST_FONT_METADATA
193
+
194
+ if exclude_upstream is not None:
195
+ # Do not use or update cache when exclude_upstream is provided.
196
+ return await _get_fastest_upstream_for_font_internal(client, exclude_upstream)
197
+
198
+ async with _FASTEST_FONT_UPSTREAM_LOCK:
199
+ if _FASTEST_FONT_UPSTREAM is not None:
200
+ return _FASTEST_FONT_UPSTREAM, _FASTEST_FONT_METADATA
201
+
202
+ upstream, metadata = await _get_fastest_upstream_for_font_internal(client)
203
+ if upstream is not None:
204
+ _FASTEST_FONT_UPSTREAM = upstream
205
+ _FASTEST_FONT_METADATA = metadata
206
+ logger.info(f"Fastest font upstream determined: {upstream}")
207
+ return upstream, metadata
208
+
209
+
210
+ async def get_fastest_upstream_for_model(client: httpx.AsyncClient | None = None):
211
+ return await get_fastest_upstream_for_font(client, exclude_upstream=["github"])
212
+
213
+
214
+ async def get_fastest_upstream(client: httpx.AsyncClient | None = None):
215
+ (
216
+ fastest_upstream_for_font,
217
+ online_font_metadata,
218
+ ) = await get_fastest_upstream_for_font(client)
219
+ if fastest_upstream_for_font is None:
220
+ logger.error("Failed to get fastest upstream")
221
+ exit(1)
222
+
223
+ if fastest_upstream_for_font == "github":
224
+ # since github is only store font, we need to get the fastest upstream for model
225
+ fastest_upstream_for_model, _ = await get_fastest_upstream_for_model(client)
226
+ if fastest_upstream_for_model is None:
227
+ logger.error("Failed to get fastest upstream")
228
+ exit(1)
229
+ else:
230
+ fastest_upstream_for_model = fastest_upstream_for_font
231
+
232
+ return online_font_metadata, fastest_upstream_for_font, fastest_upstream_for_model
233
+
234
+
235
+ async def get_doclayout_onnx_model_path_async(client: httpx.AsyncClient | None = None):
236
+ onnx_path = get_cache_file_path(
237
+ "doclayout_yolo_docstructbench_imgsz1024.onnx", "models"
238
+ )
239
+ if verify_file(onnx_path, DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256):
240
+ return onnx_path
241
+
242
+ logger.info("doclayout onnx model not found or corrupted, downloading...")
243
+ fastest_upstream, _ = await get_fastest_upstream_for_model(client)
244
+ if fastest_upstream is None:
245
+ logger.error("Failed to get fastest upstream")
246
+ exit(1)
247
+
248
+ url = DOC_LAYOUT_ONNX_MODEL_URL[fastest_upstream]
249
+
250
+ await download_file(
251
+ client, url, onnx_path, DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256
252
+ )
253
+ logger.info(f"Download doclayout onnx model from {fastest_upstream} success")
254
+ return onnx_path
255
+
256
+
257
+ async def get_table_detection_rapidocr_model_path_async(
258
+ client: httpx.AsyncClient | None = None,
259
+ ):
260
+ _ = client
261
+ dummy_path = get_cache_file_path("rapidocr-retired-dummy.txt", "models")
262
+ dummy_path.write_text("RapidOCR table detection is retired.\n", encoding="utf-8")
263
+ return dummy_path
264
+
265
+
266
+ def get_doclayout_onnx_model_path():
267
+ return run_coro(get_doclayout_onnx_model_path_async())
268
+
269
+
270
+ def get_table_detection_rapidocr_model_path():
271
+ return run_coro(get_table_detection_rapidocr_model_path_async())
272
+
273
+
274
+ def get_font_url_by_name_and_upstream(font_file_name: str, upstream: str):
275
+ if upstream not in FONT_URL_BY_UPSTREAM:
276
+ logger.critical(f"Invalid upstream: {upstream}")
277
+ exit(1)
278
+
279
+ return FONT_URL_BY_UPSTREAM[upstream](font_file_name)
280
+
281
+
282
+ async def get_font_and_metadata_async(
283
+ font_file_name: str,
284
+ client: httpx.AsyncClient | None = None,
285
+ fastest_upstream: str | None = None,
286
+ font_metadata: dict | None = None,
287
+ ):
288
+ cache_file_path = get_cache_file_path(font_file_name, "fonts")
289
+ if font_file_name in EMBEDDING_FONT_METADATA and verify_file(
290
+ cache_file_path, EMBEDDING_FONT_METADATA[font_file_name]["sha3_256"]
291
+ ):
292
+ return cache_file_path, EMBEDDING_FONT_METADATA[font_file_name]
293
+
294
+ logger.info(f"Font {cache_file_path} not found or corrupted, downloading...")
295
+ if fastest_upstream is None:
296
+ fastest_upstream, font_metadata = await get_fastest_upstream_for_font(client)
297
+ if fastest_upstream is None:
298
+ logger.critical("Failed to get fastest upstream")
299
+ exit(1)
300
+
301
+ if font_file_name not in font_metadata:
302
+ logger.critical(f"Font {font_file_name} not found in {font_metadata}")
303
+ exit(1)
304
+
305
+ if verify_file(cache_file_path, font_metadata[font_file_name]["sha3_256"]):
306
+ return cache_file_path, font_metadata[font_file_name]
307
+
308
+ assert font_metadata is not None
309
+ logger.info(f"download {font_file_name} from {fastest_upstream}")
310
+
311
+ url = get_font_url_by_name_and_upstream(font_file_name, fastest_upstream)
312
+ if "sha3_256" not in font_metadata[font_file_name]:
313
+ logger.critical(f"Font {font_file_name} not found in {font_metadata}")
314
+ exit(1)
315
+ await download_file(
316
+ client, url, cache_file_path, font_metadata[font_file_name]["sha3_256"]
317
+ )
318
+ return cache_file_path, font_metadata[font_file_name]
319
+
320
+
321
+ def get_font_and_metadata(font_file_name: str):
322
+ return run_coro(get_font_and_metadata_async(font_file_name))
323
+
324
+
325
+ async def get_cmap_file_path_async(
326
+ name: str, client: httpx.AsyncClient | None = None
327
+ ) -> Path:
328
+ """Get cached cmap file path, downloading it if necessary."""
329
+ if name.endswith(".json"):
330
+ file_name = name
331
+ else:
332
+ file_name = f"{name}.json"
333
+
334
+ if file_name not in CMAP_METADATA:
335
+ logger.critical(f"CMap {file_name} not found in CMAP_METADATA")
336
+ exit(1)
337
+
338
+ meta = CMAP_METADATA[file_name]
339
+ cache_file_path = get_cache_file_path(file_name, "cmap")
340
+ if verify_file(cache_file_path, meta["sha3_256"]):
341
+ return cache_file_path
342
+
343
+ logger.info(f"CMap {cache_file_path} not found or corrupted, downloading...")
344
+ await download_cmap_file_async(file_name, client)
345
+ if not verify_file(cache_file_path, meta["sha3_256"]):
346
+ logger.critical(f"Failed to verify downloaded cmap file: {cache_file_path}")
347
+ exit(1)
348
+ return cache_file_path
349
+
350
+
351
+ async def download_cmap_file_async(
352
+ file_name: str, client: httpx.AsyncClient | None = None
353
+ ) -> Path:
354
+ """Download a single cmap file to cache directory."""
355
+ if file_name not in CMAP_METADATA:
356
+ logger.critical(f"CMap {file_name} not found in CMAP_METADATA")
357
+ exit(1)
358
+
359
+ fastest_upstream, _ = await get_fastest_upstream_for_font(client)
360
+ if fastest_upstream is None:
361
+ logger.critical("Failed to get fastest upstream for cmap")
362
+ exit(1)
363
+
364
+ if fastest_upstream not in CMAP_URL_BY_UPSTREAM:
365
+ logger.critical(f"Invalid fastest upstream for cmap: {fastest_upstream}")
366
+ exit(1)
367
+
368
+ url = CMAP_URL_BY_UPSTREAM[fastest_upstream](file_name)
369
+ cache_file_path = get_cache_file_path(file_name, "cmap")
370
+ sha3_256 = CMAP_METADATA[file_name]["sha3_256"]
371
+ await download_file(client, url, cache_file_path, sha3_256)
372
+ return cache_file_path
373
+
374
+
375
+ async def get_cmap_data_async(
376
+ name: str, client: httpx.AsyncClient | None = None
377
+ ) -> dict:
378
+ """Load cmap json data from cached file, downloading it if necessary."""
379
+ path = await get_cmap_file_path_async(name, client)
380
+ return json.loads(path.read_text())
381
+
382
+
383
+ def get_cmap_file_path(name: str):
384
+ return run_coro(get_cmap_file_path_async(name))
385
+
386
+
387
+ def get_cmap_data(name: str):
388
+ return run_coro(get_cmap_data_async(name))
389
+
390
+
391
+ def get_font_family(lang_code: str):
392
+ font_family = embedding_assets_metadata.get_font_family(lang_code)
393
+ return font_family
394
+
395
+
396
+ async def download_all_fonts_async(client: httpx.AsyncClient | None = None):
397
+ for font_file_name in EMBEDDING_FONT_METADATA:
398
+ if not verify_file(
399
+ get_cache_file_path(font_file_name, "fonts"),
400
+ EMBEDDING_FONT_METADATA[font_file_name]["sha3_256"],
401
+ ):
402
+ break
403
+ else:
404
+ logger.debug("All fonts are already downloaded")
405
+ return
406
+
407
+ fastest_upstream, font_metadata = await get_fastest_upstream_for_font(client)
408
+ if fastest_upstream is None:
409
+ logger.error("Failed to get fastest upstream")
410
+ exit(1)
411
+ logger.info(f"Downloading fonts from {fastest_upstream}")
412
+
413
+ font_tasks = [
414
+ asyncio.create_task(
415
+ get_font_and_metadata_async(
416
+ font_file_name, client, fastest_upstream, font_metadata
417
+ )
418
+ )
419
+ for font_file_name in EMBEDDING_FONT_METADATA
420
+ ]
421
+ await asyncio.gather(*font_tasks)
422
+
423
+
424
+ async def download_all_cmaps_async(client: httpx.AsyncClient | None = None):
425
+ """Download all cmap files defined in CMAP_METADATA."""
426
+ for cmap_file_name, meta in CMAP_METADATA.items():
427
+ if not verify_file(
428
+ get_cache_file_path(cmap_file_name, "cmap"),
429
+ meta["sha3_256"],
430
+ ):
431
+ break
432
+ else:
433
+ logger.debug("All cmaps are already downloaded")
434
+ return
435
+
436
+ fastest_upstream, _ = await get_fastest_upstream_for_font(client)
437
+ if fastest_upstream is None:
438
+ logger.error("Failed to get fastest upstream for cmap")
439
+ exit(1)
440
+ logger.info(f"Downloading cmaps from {fastest_upstream}")
441
+
442
+ cmap_tasks = [
443
+ asyncio.create_task(get_cmap_file_path_async(cmap_file_name, client))
444
+ for cmap_file_name in CMAP_METADATA
445
+ ]
446
+ await asyncio.gather(*cmap_tasks)
447
+
448
+
449
+ async def async_warmup():
450
+ logger.info("Downloading all assets...")
451
+ from tiktoken import encoding_for_model
452
+
453
+ _ = encoding_for_model("gpt-4o")
454
+ async with httpx.AsyncClient() as client:
455
+ onnx_task = asyncio.create_task(get_doclayout_onnx_model_path_async(client))
456
+ font_tasks = asyncio.create_task(download_all_fonts_async(client))
457
+ cmap_tasks = asyncio.create_task(download_all_cmaps_async(client))
458
+ await asyncio.gather(onnx_task, font_tasks, cmap_tasks)
459
+
460
+
461
+ def warmup():
462
+ run_coro(async_warmup())
463
+
464
+
465
+ def generate_all_assets_file_list():
466
+ result: dict[str, list[dict[str, str]]] = {}
467
+ result["fonts"] = []
468
+ result["models"] = []
469
+ result["tiktoken"] = []
470
+ result["cmap"] = []
471
+ for font_file_name in EMBEDDING_FONT_METADATA:
472
+ result["fonts"].append(
473
+ {
474
+ "name": font_file_name,
475
+ "sha3_256": EMBEDDING_FONT_METADATA[font_file_name]["sha3_256"],
476
+ }
477
+ )
478
+ for cmap_file_name in CMAP_METADATA:
479
+ result["cmap"].append(
480
+ {
481
+ "name": cmap_file_name,
482
+ "sha3_256": CMAP_METADATA[cmap_file_name]["sha3_256"],
483
+ }
484
+ )
485
+ for tiktoken_file, sha3_256 in TIKTOKEN_CACHES.items():
486
+ result["tiktoken"].append(
487
+ {
488
+ "name": tiktoken_file,
489
+ "sha3_256": sha3_256,
490
+ }
491
+ )
492
+ result["models"].append(
493
+ {
494
+ "name": "doclayout_yolo_docstructbench_imgsz1024.onnx",
495
+ "sha3_256": DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256,
496
+ },
497
+ )
498
+ return result
499
+
500
+
501
+ async def generate_offline_assets_package_async(output_directory: Path | None = None):
502
+ await async_warmup()
503
+ logger.info("Generating offline assets package...")
504
+ file_list = generate_all_assets_file_list()
505
+ offline_assets_tag = get_offline_assets_tag(file_list)
506
+ if output_directory is None:
507
+ output_path = get_cache_file_path(
508
+ f"offline_assets_{offline_assets_tag}.zip", "assets"
509
+ )
510
+ else:
511
+ output_directory.mkdir(parents=True, exist_ok=True)
512
+ output_path = output_directory / f"offline_assets_{offline_assets_tag}.zip"
513
+ with zipfile.ZipFile(
514
+ output_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9
515
+ ) as zipf:
516
+ for file_type, file_descs in file_list.items():
517
+ # zipf.mkdir(file_type)
518
+ for file_desc in file_descs:
519
+ file_name = file_desc["name"]
520
+ sha3_256 = file_desc["sha3_256"]
521
+ file_path = get_cache_file_path(file_name, file_type)
522
+ if not verify_file(file_path, sha3_256):
523
+ logger.error(f"File {file_path} is corrupted")
524
+ exit(1)
525
+
526
+ with file_path.open("rb") as f:
527
+ zipf.writestr(f"{file_type}/{file_name}", f.read())
528
+ logger.info(f"Offline assets package generated at {output_path}")
529
+
530
+
531
+ async def restore_offline_assets_package_async(input_path: Path | None = None):
532
+ file_list = generate_all_assets_file_list()
533
+ offline_assets_tag = get_offline_assets_tag(file_list)
534
+ if input_path is None:
535
+ input_path = get_cache_file_path(
536
+ f"offline_assets_{offline_assets_tag}.zip", "assets"
537
+ )
538
+ else:
539
+ if input_path.exists() and input_path.is_dir():
540
+ input_path = input_path / f"offline_assets_{offline_assets_tag}.zip"
541
+ if not input_path.exists():
542
+ logger.critical(f"Offline assets package not found: {input_path}")
543
+ exit(1)
544
+
545
+ import re
546
+
547
+ offline_assets_tag_from_input_path = re.match(
548
+ r"offline_assets_(.*)\.zip", input_path.name
549
+ ).group(1)
550
+ if offline_assets_tag != offline_assets_tag_from_input_path:
551
+ logger.critical(
552
+ f"Offline assets tag mismatch: {offline_assets_tag} != {offline_assets_tag_from_input_path}"
553
+ )
554
+ exit(1)
555
+ nothing_changed = True
556
+ with zipfile.ZipFile(input_path, "r") as zipf:
557
+ for file_type, file_descs in file_list.items():
558
+ for file_desc in file_descs:
559
+ file_name = file_desc["name"]
560
+ file_path = get_cache_file_path(file_name, file_type)
561
+
562
+ if verify_file(file_path, file_desc["sha3_256"]):
563
+ continue
564
+ nothing_changed = False
565
+ with zipf.open(f"{file_type}/{file_name}", "r") as f:
566
+ with file_path.open("wb") as f2:
567
+ f2.write(f.read())
568
+ if not verify_file(file_path, file_desc["sha3_256"]):
569
+ logger.critical(
570
+ "Offline assets package is corrupted, please delete it and try again"
571
+ )
572
+ exit(1)
573
+ if not nothing_changed:
574
+ logger.info(f"Offline assets package restored from {input_path}")
575
+
576
+
577
+ def get_offline_assets_tag(file_list: dict | None = None):
578
+ if file_list is None:
579
+ file_list = generate_all_assets_file_list()
580
+ import orjson
581
+
582
+ # noinspection PyTypeChecker
583
+ offline_assets_tag = hashlib.sha3_256(
584
+ orjson.dumps(
585
+ file_list,
586
+ option=orjson.OPT_APPEND_NEWLINE
587
+ | orjson.OPT_INDENT_2
588
+ | orjson.OPT_SORT_KEYS,
589
+ )
590
+ ).hexdigest()
591
+ return offline_assets_tag
592
+
593
+
594
+ def generate_offline_assets_package(output_directory: Path | None = None):
595
+ return run_coro(generate_offline_assets_package_async(output_directory))
596
+
597
+
598
+ def restore_offline_assets_package(input_path: Path | None = None):
599
+ return run_coro(restore_offline_assets_package_async(input_path))
600
+
601
+
602
+ if __name__ == "__main__":
603
+ from rich.logging import RichHandler
604
+
605
+ logging.basicConfig(level=logging.DEBUG, handlers=[RichHandler()])
606
+ logging.getLogger("httpx").setLevel(logging.WARNING)
607
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
608
+ # warmup()
609
+ # generate_offline_assets_package()
610
+ # warmup()