BabelDOC 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {babeldoc-0.2.1 → babeldoc-0.2.2}/PKG-INFO +3 -1
- babeldoc-0.2.2/babeldoc/__init__.py +1 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/assets/assets.py +12 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/assets/embedding_assets_metadata.py +3 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/const.py +6 -1
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/il_version_1.py +6 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/il_version_1.rnc +1 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/il_version_1.rng +5 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/il_version_1.xsd +1 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/il_translator.py +60 -36
- babeldoc-0.2.2/babeldoc/document_il/midend/il_translator_llm_only.py +375 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/paragraph_finder.py +7 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/styles_and_formulas.py +4 -4
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/translator/translator.py +65 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/utils/fontmap.py +1 -1
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/docvision/rpc_doclayout.py +4 -4
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/high_level.py +16 -1
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/main.py +1 -1
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/progress_monitor.py +43 -20
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/CONTRIBUTING.md +3 -3
- {babeldoc-0.2.1 → babeldoc-0.2.2}/pyproject.toml +4 -2
- {babeldoc-0.2.1 → babeldoc-0.2.2}/uv.lock +234 -1
- babeldoc-0.2.1/babeldoc/__init__.py +0 -1
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.cursorignore +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/dependabot.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/labels.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/release-drafter.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/workflows/codeql.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/workflows/docs.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/workflows/labeler.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/workflows/publish-to-pypi.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.github/workflows/test.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.gitignore +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/.pre-commit-config.yaml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/LICENSE +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/README.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/asynchronize/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/converter.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/backend/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/backend/pdf_creater.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/frontend/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/frontend/il_creater.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/add_debug_information.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/layout_parser.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/remove_descent.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/midend/typesetting.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/translator/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/translator/cache.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/utils/layout_helper.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/utils/style_helper.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/document_il/xml_converter.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/docvision/README.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/docvision/__init__.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/docvision/doclayout.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/pdfinterp.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/tools/generate_font_metadata.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/babeldoc/translation_config.py +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/CODE_OF_CONDUCT.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/CONTRIBUTOR_REWARD.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/README.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/README.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/deploy.sh +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-banner.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-big-logo.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-contributor_reward_example.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-preview.gif +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/images/babeldoc-small-logo.png +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/index.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/intro-to-pdf-object.md +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/docs/requirements.txt +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/mkdocs.yml +0 -0
- {babeldoc-0.2.1 → babeldoc-0.2.2}/tests/test_translation_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BabelDOC
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Yet Another Document Translator
|
|
5
5
|
Project-URL: Homepage, https://github.com/funstory-ai/BabelDOC
|
|
6
6
|
Project-URL: Issues, https://github.com/funstory-ai/BabelDOC/issues
|
|
@@ -26,9 +26,11 @@ Requires-Dist: pdfminer-six>=20240706
|
|
|
26
26
|
Requires-Dist: peewee>=3.17.8
|
|
27
27
|
Requires-Dist: pydantic>=2.10.6
|
|
28
28
|
Requires-Dist: pymupdf>=1.25.1
|
|
29
|
+
Requires-Dist: python-levenshtein>=0.27.1
|
|
29
30
|
Requires-Dist: rich>=13.9.4
|
|
30
31
|
Requires-Dist: scikit-image>=0.25.2
|
|
31
32
|
Requires-Dist: tenacity>=9.0.0
|
|
33
|
+
Requires-Dist: tiktoken>=0.9.0
|
|
32
34
|
Requires-Dist: toml>=0.10.2
|
|
33
35
|
Requires-Dist: tqdm>=4.67.1
|
|
34
36
|
Requires-Dist: xsdata[cli,lxml,soap]>=24.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -14,6 +14,7 @@ from babeldoc.assets.embedding_assets_metadata import (
|
|
|
14
14
|
from babeldoc.assets.embedding_assets_metadata import EMBEDDING_FONT_METADATA
|
|
15
15
|
from babeldoc.assets.embedding_assets_metadata import FONT_METADATA_URL
|
|
16
16
|
from babeldoc.assets.embedding_assets_metadata import FONT_URL_BY_UPSTREAM
|
|
17
|
+
from babeldoc.assets.embedding_assets_metadata import TIKTOKEN_CACHES
|
|
17
18
|
from babeldoc.const import get_cache_file_path
|
|
18
19
|
from tenacity import retry
|
|
19
20
|
from tenacity import stop_after_attempt
|
|
@@ -292,6 +293,9 @@ async def download_all_fonts_async(client: httpx.AsyncClient | None = None):
|
|
|
292
293
|
|
|
293
294
|
async def async_warmup():
|
|
294
295
|
logger.info("Downloading all assets...")
|
|
296
|
+
from tiktoken import encoding_for_model
|
|
297
|
+
|
|
298
|
+
_ = encoding_for_model("gpt-4o")
|
|
295
299
|
async with httpx.AsyncClient() as client:
|
|
296
300
|
onnx_task = asyncio.create_task(get_doclayout_onnx_model_path_async(client))
|
|
297
301
|
font_tasks = asyncio.create_task(download_all_fonts_async(client))
|
|
@@ -306,6 +310,7 @@ def generate_all_assets_file_list():
|
|
|
306
310
|
result = {}
|
|
307
311
|
result["fonts"] = []
|
|
308
312
|
result["models"] = []
|
|
313
|
+
result["tiktoken"] = []
|
|
309
314
|
for font_file_name in EMBEDDING_FONT_METADATA:
|
|
310
315
|
result["fonts"].append(
|
|
311
316
|
{
|
|
@@ -313,6 +318,13 @@ def generate_all_assets_file_list():
|
|
|
313
318
|
"sha3_256": EMBEDDING_FONT_METADATA[font_file_name]["sha3_256"],
|
|
314
319
|
}
|
|
315
320
|
)
|
|
321
|
+
for tiktoken_file, sha3_256 in TIKTOKEN_CACHES.items():
|
|
322
|
+
result["tiktoken"].append(
|
|
323
|
+
{
|
|
324
|
+
"name": tiktoken_file,
|
|
325
|
+
"sha3_256": sha3_256,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
316
328
|
result["models"].append(
|
|
317
329
|
{
|
|
318
330
|
"name": "doclayout_yolo_docstructbench_imgsz1024.onnx",
|
|
@@ -2,6 +2,9 @@ DOCLAYOUT_YOLO_DOCSTRUCTBENCH_IMGSZ1024ONNX_SHA3_256 = (
|
|
|
2
2
|
"60be061226930524958b5465c8c04af3d7c03bcb0beb66454f5da9f792e3cf2a"
|
|
3
3
|
)
|
|
4
4
|
|
|
5
|
+
TIKTOKEN_CACHES = {
|
|
6
|
+
"fb374d419588a4632f3f557e76b4b70aebbca790": "cb04bcda5782cfbbe77f2f991d92c0ea785d9496ef1137c91dfc3c8c324528d6"
|
|
7
|
+
}
|
|
5
8
|
|
|
6
9
|
FONT_METADATA_URL = {
|
|
7
10
|
"github": "https://raw.githubusercontent.com/funstory-ai/BabelDOC-Assets/refs/heads/main/font_metadata.json",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
|
-
__version__ = "0.2.
|
|
6
|
+
__version__ = "0.2.2"
|
|
6
7
|
|
|
7
8
|
CACHE_FOLDER = Path.home() / ".cache" / "babeldoc"
|
|
8
9
|
|
|
@@ -35,3 +36,7 @@ try:
|
|
|
35
36
|
)
|
|
36
37
|
except (OSError, FileNotFoundError, subprocess.CalledProcessError):
|
|
37
38
|
WATERMARK_VERSION = f"v{__version__}"
|
|
39
|
+
|
|
40
|
+
TIKTOKEN_CACHE_FOLDER = CACHE_FOLDER / "tiktoken"
|
|
41
|
+
TIKTOKEN_CACHE_FOLDER.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
os.environ["TIKTOKEN_CACHE_DIR"] = str(TIKTOKEN_CACHE_FOLDER)
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
<xs:attribute name="vertical" type="xs:boolean"/>
|
|
188
188
|
<xs:attribute name="FirstLineIndent" type="xs:boolean"/>
|
|
189
189
|
<xs:attribute name="debug_id" type="xs:string"/>
|
|
190
|
+
<xs:attribute name="layout_label" type="xs:string"/>
|
|
190
191
|
</xs:complexType>
|
|
191
192
|
</xs:element>
|
|
192
193
|
<xs:element name="pdfParagraphComposition">
|
|
@@ -524,6 +524,56 @@ class ILTranslator:
|
|
|
524
524
|
|
|
525
525
|
return result
|
|
526
526
|
|
|
527
|
+
def pre_translate_paragraph(
|
|
528
|
+
self,
|
|
529
|
+
paragraph: PdfParagraph,
|
|
530
|
+
tracker: ParagraphTranslateTracker,
|
|
531
|
+
page_font_map: dict[str, PdfFont],
|
|
532
|
+
xobj_font_map: dict[int, dict[str, PdfFont]],
|
|
533
|
+
):
|
|
534
|
+
"""Pre-translation processing: prepare text for translation."""
|
|
535
|
+
if paragraph.vertical:
|
|
536
|
+
return None, None
|
|
537
|
+
tracker.set_pdf_unicode(paragraph.unicode)
|
|
538
|
+
if paragraph.xobj_id in xobj_font_map:
|
|
539
|
+
page_font_map = xobj_font_map[paragraph.xobj_id]
|
|
540
|
+
translate_input = self.get_translate_input(paragraph, page_font_map)
|
|
541
|
+
if not translate_input:
|
|
542
|
+
return None, None
|
|
543
|
+
tracker.set_input(translate_input.unicode)
|
|
544
|
+
text = translate_input.unicode
|
|
545
|
+
if len(text) < self.translation_config.min_text_length:
|
|
546
|
+
logger.debug(
|
|
547
|
+
f"Text too short to translate, skip. Text: {text}. Paragraph id: {paragraph.debug_id}."
|
|
548
|
+
)
|
|
549
|
+
return None, None
|
|
550
|
+
return text, translate_input
|
|
551
|
+
|
|
552
|
+
def post_translate_paragraph(
|
|
553
|
+
self,
|
|
554
|
+
paragraph: PdfParagraph,
|
|
555
|
+
tracker: ParagraphTranslateTracker,
|
|
556
|
+
translate_input,
|
|
557
|
+
translated_text: str,
|
|
558
|
+
):
|
|
559
|
+
"""Post-translation processing: update paragraph with translated text."""
|
|
560
|
+
tracker.set_output(translated_text)
|
|
561
|
+
if translated_text == translate_input.unicode:
|
|
562
|
+
return False
|
|
563
|
+
paragraph.unicode = translated_text
|
|
564
|
+
paragraph.pdf_paragraph_composition = self.parse_translate_output(
|
|
565
|
+
translate_input, translated_text
|
|
566
|
+
)
|
|
567
|
+
for composition in paragraph.pdf_paragraph_composition:
|
|
568
|
+
if (
|
|
569
|
+
composition.pdf_same_style_unicode_characters
|
|
570
|
+
and composition.pdf_same_style_unicode_characters.pdf_style is None
|
|
571
|
+
):
|
|
572
|
+
composition.pdf_same_style_unicode_characters.pdf_style = (
|
|
573
|
+
paragraph.pdf_style
|
|
574
|
+
)
|
|
575
|
+
return True
|
|
576
|
+
|
|
527
577
|
def translate_paragraph(
|
|
528
578
|
self,
|
|
529
579
|
paragraph: PdfParagraph,
|
|
@@ -532,51 +582,25 @@ class ILTranslator:
|
|
|
532
582
|
page_font_map: dict[str, PdfFont] = None,
|
|
533
583
|
xobj_font_map: dict[int, dict[str, PdfFont]] = None,
|
|
534
584
|
):
|
|
585
|
+
"""Translate a paragraph using pre and post processing functions."""
|
|
535
586
|
self.translation_config.raise_if_cancelled()
|
|
536
587
|
with PbarContext(pbar):
|
|
537
588
|
try:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if
|
|
543
|
-
page_font_map = xobj_font_map[paragraph.xobj_id]
|
|
544
|
-
translate_input = self.get_translate_input(paragraph, page_font_map)
|
|
545
|
-
if not translate_input:
|
|
546
|
-
return
|
|
547
|
-
|
|
548
|
-
tracker.set_input(translate_input.unicode)
|
|
549
|
-
|
|
550
|
-
text = translate_input.unicode
|
|
551
|
-
|
|
552
|
-
if len(text) < self.translation_config.min_text_length:
|
|
553
|
-
logger.debug(
|
|
554
|
-
f"Text too short to translate, skip. Text: {text}. Paragraph id: {paragraph.debug_id}.",
|
|
555
|
-
)
|
|
589
|
+
# Pre-translation processing
|
|
590
|
+
text, translate_input = self.pre_translate_paragraph(
|
|
591
|
+
paragraph, tracker, page_font_map, xobj_font_map
|
|
592
|
+
)
|
|
593
|
+
if text is None:
|
|
556
594
|
return
|
|
557
595
|
|
|
596
|
+
# Perform translation
|
|
558
597
|
translated_text = self.translate_engine.translate(text)
|
|
559
598
|
translated_text = re.sub(r"[. 。…,]{20,}", ".", translated_text)
|
|
560
599
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
paragraph.unicode = translated_text
|
|
567
|
-
paragraph.pdf_paragraph_composition = self.parse_translate_output(
|
|
568
|
-
translate_input,
|
|
569
|
-
translated_text,
|
|
600
|
+
# Post-translation processing
|
|
601
|
+
self.post_translate_paragraph(
|
|
602
|
+
paragraph, tracker, translate_input, translated_text
|
|
570
603
|
)
|
|
571
|
-
for composition in paragraph.pdf_paragraph_composition:
|
|
572
|
-
if (
|
|
573
|
-
composition.pdf_same_style_unicode_characters
|
|
574
|
-
and composition.pdf_same_style_unicode_characters.pdf_style
|
|
575
|
-
is None
|
|
576
|
-
):
|
|
577
|
-
composition.pdf_same_style_unicode_characters.pdf_style = (
|
|
578
|
-
paragraph.pdf_style
|
|
579
|
-
)
|
|
580
604
|
except Exception as e:
|
|
581
605
|
logger.exception(
|
|
582
606
|
f"Error translating paragraph. Paragraph: {paragraph}. Error: {e}. ",
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import Levenshtein
|
|
8
|
+
import tiktoken
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
|
|
11
|
+
from babeldoc.document_il import Document
|
|
12
|
+
from babeldoc.document_il import Page
|
|
13
|
+
from babeldoc.document_il import PdfFont
|
|
14
|
+
from babeldoc.document_il import PdfParagraph
|
|
15
|
+
from babeldoc.document_il.midend.il_translator import DocumentTranslateTracker
|
|
16
|
+
from babeldoc.document_il.midend.il_translator import ILTranslator
|
|
17
|
+
from babeldoc.document_il.midend.il_translator import PageTranslateTracker
|
|
18
|
+
from babeldoc.document_il.translator.translator import BaseTranslator
|
|
19
|
+
from babeldoc.document_il.utils.fontmap import FontMapper
|
|
20
|
+
from babeldoc.translation_config import TranslationConfig
|
|
21
|
+
|
|
22
|
+
enc = tiktoken.encoding_for_model("gpt-4o")
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BatchParagraph:
|
|
28
|
+
def __init__(
|
|
29
|
+
self, paragraphs: list[PdfParagraph], page_tracker: PageTranslateTracker
|
|
30
|
+
):
|
|
31
|
+
self.paragraphs = paragraphs
|
|
32
|
+
self.trackers = [page_tracker.new_paragraph() for _ in paragraphs]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ILTranslatorLLMOnly:
|
|
36
|
+
stage_name = "Translate Paragraphs"
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
translate_engine: BaseTranslator,
|
|
41
|
+
translation_config: TranslationConfig,
|
|
42
|
+
):
|
|
43
|
+
self.translate_engine = translate_engine
|
|
44
|
+
self.translation_config = translation_config
|
|
45
|
+
self.font_mapper = FontMapper(translation_config)
|
|
46
|
+
|
|
47
|
+
self.il_translator = ILTranslator(
|
|
48
|
+
translate_engine=translate_engine,
|
|
49
|
+
translation_config=translation_config,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
self.translate_engine.do_llm_translate(None)
|
|
54
|
+
except NotImplementedError as e:
|
|
55
|
+
raise ValueError("LLM translator not supported") from e
|
|
56
|
+
|
|
57
|
+
def find_title_paragraph(self, docs: Document) -> PdfParagraph | None:
|
|
58
|
+
"""Find the first paragraph with layout_label 'title' in the document.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
docs: The document to search in
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The first title paragraph found, or None if no title paragraph exists
|
|
65
|
+
"""
|
|
66
|
+
for page in docs.page:
|
|
67
|
+
for paragraph in page.pdf_paragraph:
|
|
68
|
+
if paragraph.layout_label == "title":
|
|
69
|
+
logger.info(f"Found title paragraph: {paragraph.unicode}")
|
|
70
|
+
return paragraph
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def translate(self, docs: Document) -> None:
|
|
74
|
+
tracker = DocumentTranslateTracker()
|
|
75
|
+
|
|
76
|
+
# Try to find the first title paragraph
|
|
77
|
+
title_paragraph = self.find_title_paragraph(docs)
|
|
78
|
+
|
|
79
|
+
# count total paragraph
|
|
80
|
+
total = sum(
|
|
81
|
+
[
|
|
82
|
+
len([p for p in page.pdf_paragraph if p.debug_id is not None])
|
|
83
|
+
for page in docs.page
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
with self.translation_config.progress_monitor.stage_start(
|
|
87
|
+
self.stage_name,
|
|
88
|
+
total,
|
|
89
|
+
) as pbar:
|
|
90
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
91
|
+
max_workers=self.translation_config.qps,
|
|
92
|
+
) as executor2:
|
|
93
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
94
|
+
max_workers=self.translation_config.qps,
|
|
95
|
+
) as executor:
|
|
96
|
+
for page in docs.page:
|
|
97
|
+
self.process_page(
|
|
98
|
+
page,
|
|
99
|
+
executor,
|
|
100
|
+
pbar,
|
|
101
|
+
tracker.new_page(),
|
|
102
|
+
title_paragraph,
|
|
103
|
+
executor2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
path = self.translation_config.get_working_file_path("translate_tracking.json")
|
|
107
|
+
|
|
108
|
+
if self.translation_config.debug:
|
|
109
|
+
logger.debug(f"save translate tracking to {path}")
|
|
110
|
+
with Path(path).open("w", encoding="utf-8") as f:
|
|
111
|
+
f.write(tracker.to_json())
|
|
112
|
+
|
|
113
|
+
def process_page(
|
|
114
|
+
self,
|
|
115
|
+
page: Page,
|
|
116
|
+
executor: concurrent.futures.ThreadPoolExecutor,
|
|
117
|
+
pbar: tqdm | None = None,
|
|
118
|
+
tracker: PageTranslateTracker = None,
|
|
119
|
+
title_paragraph: PdfParagraph | None = None,
|
|
120
|
+
executor2: concurrent.futures.ThreadPoolExecutor | None = None,
|
|
121
|
+
):
|
|
122
|
+
self.translation_config.raise_if_cancelled()
|
|
123
|
+
page_font_map = {}
|
|
124
|
+
for font in page.pdf_font:
|
|
125
|
+
page_font_map[font.font_id] = font
|
|
126
|
+
page_xobj_font_map = {}
|
|
127
|
+
for xobj in page.pdf_xobject:
|
|
128
|
+
page_xobj_font_map[xobj.xobj_id] = page_font_map.copy()
|
|
129
|
+
for font in xobj.pdf_font:
|
|
130
|
+
page_xobj_font_map[xobj.xobj_id][font.font_id] = font
|
|
131
|
+
|
|
132
|
+
paragraphs = []
|
|
133
|
+
local_title_paragraph = None
|
|
134
|
+
|
|
135
|
+
total_unicode_counts = 0
|
|
136
|
+
for paragraph in page.pdf_paragraph:
|
|
137
|
+
if paragraph.debug_id is None:
|
|
138
|
+
continue
|
|
139
|
+
# self.translate_paragraph(paragraph, pbar,tracker.new_paragraph(), page_font_map, page_xobj_font_map)
|
|
140
|
+
total_unicode_counts += len(paragraph.unicode)
|
|
141
|
+
paragraphs.append(paragraph)
|
|
142
|
+
if paragraph.layout_label == "title":
|
|
143
|
+
local_title_paragraph = paragraph
|
|
144
|
+
|
|
145
|
+
if total_unicode_counts > 1200 or len(paragraphs) > 4:
|
|
146
|
+
executor.submit(
|
|
147
|
+
self.translate_paragraph,
|
|
148
|
+
BatchParagraph(paragraphs, tracker),
|
|
149
|
+
pbar,
|
|
150
|
+
page_font_map,
|
|
151
|
+
page_xobj_font_map,
|
|
152
|
+
title_paragraph,
|
|
153
|
+
local_title_paragraph,
|
|
154
|
+
executor2,
|
|
155
|
+
)
|
|
156
|
+
paragraphs = []
|
|
157
|
+
total_unicode_counts = 0
|
|
158
|
+
|
|
159
|
+
if paragraphs:
|
|
160
|
+
executor.submit(
|
|
161
|
+
self.translate_paragraph,
|
|
162
|
+
BatchParagraph(paragraphs, tracker),
|
|
163
|
+
pbar,
|
|
164
|
+
page_font_map,
|
|
165
|
+
page_xobj_font_map,
|
|
166
|
+
title_paragraph,
|
|
167
|
+
local_title_paragraph,
|
|
168
|
+
executor2,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def translate_paragraph(
|
|
172
|
+
self,
|
|
173
|
+
batch_paragraph: BatchParagraph,
|
|
174
|
+
pbar: tqdm | None = None,
|
|
175
|
+
page_font_map: dict[str, PdfFont] = None,
|
|
176
|
+
xobj_font_map: dict[int, dict[str, PdfFont]] = None,
|
|
177
|
+
title_paragraph: PdfParagraph | None = None,
|
|
178
|
+
local_title_paragraph: PdfParagraph | None = None,
|
|
179
|
+
executor=None,
|
|
180
|
+
):
|
|
181
|
+
"""Translate a paragraph using pre and post processing functions."""
|
|
182
|
+
self.translation_config.raise_if_cancelled()
|
|
183
|
+
try:
|
|
184
|
+
inputs = []
|
|
185
|
+
|
|
186
|
+
for i in range(len(batch_paragraph.paragraphs)):
|
|
187
|
+
paragraph = batch_paragraph.paragraphs[i]
|
|
188
|
+
tracker = batch_paragraph.trackers[i]
|
|
189
|
+
text, translate_input = self.il_translator.pre_translate_paragraph(
|
|
190
|
+
paragraph, tracker, page_font_map, xobj_font_map
|
|
191
|
+
)
|
|
192
|
+
if text is None:
|
|
193
|
+
pbar.advance(1)
|
|
194
|
+
continue
|
|
195
|
+
inputs.append((text, translate_input, paragraph, tracker))
|
|
196
|
+
if not inputs:
|
|
197
|
+
return
|
|
198
|
+
json_format_input = []
|
|
199
|
+
|
|
200
|
+
for id_, input_text in enumerate(inputs):
|
|
201
|
+
json_format_input.append(
|
|
202
|
+
{
|
|
203
|
+
"id": id_,
|
|
204
|
+
"input": input_text[0],
|
|
205
|
+
"layout_label": input_text[2].layout_label,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
json_format_input = json.dumps(
|
|
209
|
+
json_format_input, ensure_ascii=False, indent=2
|
|
210
|
+
)
|
|
211
|
+
llm_input = [
|
|
212
|
+
"You are a professional, authentic machine translation engine."
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
if title_paragraph:
|
|
216
|
+
llm_input.append(
|
|
217
|
+
f"The first title in the full text: {title_paragraph.unicode}"
|
|
218
|
+
)
|
|
219
|
+
if (
|
|
220
|
+
local_title_paragraph
|
|
221
|
+
and local_title_paragraph.debug_id != title_paragraph.debug_id
|
|
222
|
+
):
|
|
223
|
+
llm_input.append(
|
|
224
|
+
f"The most similar title in the full text: {local_title_paragraph.unicode}"
|
|
225
|
+
)
|
|
226
|
+
# Create a structured prompt template for LLM translation
|
|
227
|
+
prompt_template = (
|
|
228
|
+
f"""
|
|
229
|
+
You will be given a JSON formatted input containing entries with "id" and "input" fields. Here is the input:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{json_format_input}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For each entry in the JSON, translate the contents of the "input" field into {self.translation_config.lang_out}.
|
|
236
|
+
Write the translation back into the "output" field for that entry.
|
|
237
|
+
|
|
238
|
+
"""
|
|
239
|
+
+ """
|
|
240
|
+
Here is an example of the expected format:
|
|
241
|
+
|
|
242
|
+
<example>
|
|
243
|
+
```json
|
|
244
|
+
Input:
|
|
245
|
+
{
|
|
246
|
+
"id": 1,
|
|
247
|
+
"input": "Source",
|
|
248
|
+
"layout_label": "plain text"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
Output:
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"id": 1,
|
|
255
|
+
"output": "Translation"
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
</example>
|
|
259
|
+
|
|
260
|
+
Please return the translated json directly without wrapping ```json``` tag or include any additional information.
|
|
261
|
+
"""
|
|
262
|
+
)
|
|
263
|
+
llm_input.append(prompt_template)
|
|
264
|
+
|
|
265
|
+
final_input = "\n".join(llm_input).strip()
|
|
266
|
+
llm_output = self.translate_engine.llm_translate(final_input)
|
|
267
|
+
llm_output = llm_output.strip()
|
|
268
|
+
|
|
269
|
+
llm_output = self._clean_json_output(llm_output)
|
|
270
|
+
|
|
271
|
+
parsed_output = json.loads(llm_output)
|
|
272
|
+
|
|
273
|
+
translation_results = {item["id"]: item["output"] for item in parsed_output}
|
|
274
|
+
|
|
275
|
+
if len(translation_results) != len(inputs):
|
|
276
|
+
raise Exception(
|
|
277
|
+
f"Translation results length mismatch. Expected: {len(inputs)}, Got: {len(translation_results)}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
for id_, output in translation_results.items():
|
|
281
|
+
should_fallback = True
|
|
282
|
+
try:
|
|
283
|
+
if not isinstance(output, str):
|
|
284
|
+
logger.warning(
|
|
285
|
+
f"Translation result is not a string. Output: {output}"
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
id_ = int(id_) # Ensure id is an integer
|
|
290
|
+
if id_ >= len(inputs):
|
|
291
|
+
logger.warning(f"Invalid id {id_}, skipping")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
# Clean up any excessive punctuation in the translated text
|
|
295
|
+
translated_text = re.sub(r"[. 。…,]{20,}", ".", output)
|
|
296
|
+
|
|
297
|
+
# Get the original input for this translation
|
|
298
|
+
translate_input = inputs[id_][1]
|
|
299
|
+
|
|
300
|
+
input_unicode = inputs[id_][2].unicode
|
|
301
|
+
output_unicode = translated_text
|
|
302
|
+
|
|
303
|
+
input_token_count = len(enc.encode(input_unicode))
|
|
304
|
+
output_token_count = len(enc.encode(output_unicode))
|
|
305
|
+
|
|
306
|
+
if not (0.3 < output_token_count / input_token_count < 3):
|
|
307
|
+
logger.warning(
|
|
308
|
+
f"Translation result is too long or too short. Input: {input_token_count}, Output: {output_token_count}"
|
|
309
|
+
)
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
edit_distance = Levenshtein.distance(input_unicode, output_unicode)
|
|
313
|
+
if edit_distance < 5 and input_token_count > 20:
|
|
314
|
+
logger.warning(
|
|
315
|
+
f"Translation result edit distance is too small. distance: {edit_distance}, input: {input_unicode}, output: {output_unicode}"
|
|
316
|
+
)
|
|
317
|
+
continue
|
|
318
|
+
# Apply the translation to the paragraph
|
|
319
|
+
self.il_translator.post_translate_paragraph(
|
|
320
|
+
inputs[id_][2],
|
|
321
|
+
inputs[id_][3],
|
|
322
|
+
translate_input,
|
|
323
|
+
translated_text,
|
|
324
|
+
)
|
|
325
|
+
if pbar:
|
|
326
|
+
pbar.advance(1)
|
|
327
|
+
should_fallback = False
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.exception(f"Error translating paragraph. Error: {e}.")
|
|
330
|
+
# Ignore error and continue
|
|
331
|
+
continue
|
|
332
|
+
finally:
|
|
333
|
+
if should_fallback:
|
|
334
|
+
logger.warning(
|
|
335
|
+
f"Fallback to simple translation. paragraph id: {inputs[id_][2].debug_id}"
|
|
336
|
+
)
|
|
337
|
+
executor.submit(
|
|
338
|
+
self.il_translator.translate_paragraph,
|
|
339
|
+
inputs[id_][2],
|
|
340
|
+
pbar,
|
|
341
|
+
inputs[id_][3],
|
|
342
|
+
page_font_map,
|
|
343
|
+
xobj_font_map,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.warning(f"Error {e} during translation. try fallback")
|
|
348
|
+
for i in range(len(batch_paragraph.paragraphs)):
|
|
349
|
+
paragraph = batch_paragraph.paragraphs[i]
|
|
350
|
+
tracker = batch_paragraph.trackers[i]
|
|
351
|
+
if paragraph.debug_id is None:
|
|
352
|
+
continue
|
|
353
|
+
executor.submit(
|
|
354
|
+
self.il_translator.translate_paragraph,
|
|
355
|
+
paragraph,
|
|
356
|
+
pbar,
|
|
357
|
+
tracker,
|
|
358
|
+
page_font_map,
|
|
359
|
+
xobj_font_map,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def _clean_json_output(self, llm_output: str) -> str:
|
|
363
|
+
# Clean up JSON output by removing common wrapper tags
|
|
364
|
+
llm_output = llm_output.strip()
|
|
365
|
+
if llm_output.startswith("<json>"):
|
|
366
|
+
llm_output = llm_output[6:]
|
|
367
|
+
if llm_output.endswith("</json>"):
|
|
368
|
+
llm_output = llm_output[:-7]
|
|
369
|
+
if llm_output.startswith("```json"):
|
|
370
|
+
llm_output = llm_output[7:]
|
|
371
|
+
if llm_output.startswith("```"):
|
|
372
|
+
llm_output = llm_output[3:]
|
|
373
|
+
if llm_output.endswith("```"):
|
|
374
|
+
llm_output = llm_output[:-3]
|
|
375
|
+
return llm_output.strip()
|
|
@@ -177,6 +177,9 @@ class ParagraphFinder:
|
|
|
177
177
|
current_paragraph = PdfParagraph(
|
|
178
178
|
pdf_paragraph_composition=[line],
|
|
179
179
|
debug_id=generate_base58_id(),
|
|
180
|
+
layout_label=char_layout.name
|
|
181
|
+
if not current_layout
|
|
182
|
+
else current_layout.name,
|
|
180
183
|
)
|
|
181
184
|
paragraphs.append(current_paragraph)
|
|
182
185
|
else:
|
|
@@ -212,6 +215,7 @@ class ParagraphFinder:
|
|
|
212
215
|
current_paragraph = PdfParagraph(
|
|
213
216
|
pdf_paragraph_composition=[line],
|
|
214
217
|
debug_id=generate_base58_id(),
|
|
218
|
+
layout_label=current_layout.name,
|
|
215
219
|
)
|
|
216
220
|
self.update_paragraph_data(current_paragraph)
|
|
217
221
|
paragraphs.append(current_paragraph)
|
|
@@ -228,6 +232,7 @@ class ParagraphFinder:
|
|
|
228
232
|
current_paragraph = PdfParagraph(
|
|
229
233
|
pdf_paragraph_composition=[line],
|
|
230
234
|
debug_id=generate_base58_id(),
|
|
235
|
+
layout_label=current_layout.name,
|
|
231
236
|
)
|
|
232
237
|
paragraphs.append(current_paragraph)
|
|
233
238
|
else:
|
|
@@ -412,6 +417,7 @@ class ParagraphFinder:
|
|
|
412
417
|
),
|
|
413
418
|
unicode="",
|
|
414
419
|
debug_id=generate_base58_id(),
|
|
420
|
+
layout_label=prev_composition.layout_label,
|
|
415
421
|
)
|
|
416
422
|
# 更新原段落
|
|
417
423
|
paragraph.pdf_paragraph_composition = (
|
|
@@ -440,6 +446,7 @@ class ParagraphFinder:
|
|
|
440
446
|
),
|
|
441
447
|
unicode="",
|
|
442
448
|
debug_id=generate_base58_id(),
|
|
449
|
+
layout_label=prev_composition.layout_label,
|
|
443
450
|
)
|
|
444
451
|
# 更新原段落
|
|
445
452
|
paragraph.pdf_paragraph_composition = (
|