BabelDOC 0.3.55__tar.gz → 0.3.57__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.3.55 → babeldoc-0.3.57}/PKG-INFO +2 -1
- babeldoc-0.3.57/babeldoc/__init__.py +1 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/const.py +1 -1
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/backend/pdf_creater.py +2 -2
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/automatic_term_extractor.py +4 -2
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/translator/translator.py +5 -9
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/glossary.py +80 -39
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/high_level.py +18 -22
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/main.py +1 -1
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/result_merger.py +71 -36
- {babeldoc-0.3.55 → babeldoc-0.3.57}/pyproject.toml +3 -2
- {babeldoc-0.3.55 → babeldoc-0.3.57}/uv.lock +438 -333
- babeldoc-0.3.55/babeldoc/__init__.py +0 -1
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.cursorignore +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/PULL_REQUEST_TEMPLATE/pr_form.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/dependabot.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/labels.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/release-drafter.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/codeql.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/docs.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/labeler.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/lint.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/pr-lint.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/publish-to-pypi.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.github/workflows/test.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.gitignore +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/.pre-commit-config.yaml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/LICENSE +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/README.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/assets/assets.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/assets/embedding_assets_metadata.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/asynchronize/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/converter.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/babeldoc_exception/BabelDOCException.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/backend/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/frontend/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/frontend/il_creater.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/il_version_1.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/il_version_1.rnc +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/il_version_1.rng +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/il_version_1.xsd +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/add_debug_information.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/detect_scanned_file.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/il_translator.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/il_translator_llm_only.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/layout_parser.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/paragraph_finder.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/remove_descent.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/styles_and_formulas.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/table_parser.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/midend/typesetting.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/translator/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/translator/cache.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/atomic_integer.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/fontmap.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/layout_helper.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/mupdf_helper.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/paragraph_helper.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/priority_thread_pool_executor.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/style_helper.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/utils/zstd_helper.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/document_il/xml_converter.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/docvision/README.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/docvision/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/docvision/doclayout.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/docvision/rpc_doclayout.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/docvision/table_detection/rapidocr.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/format/office/__init__.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/pdfinterp.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/progress_monitor.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/split_manager.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/tools/generate_font_metadata.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/tools/italic_assistance.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/tools/italic_recognize_tool.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/babeldoc/translation_config.py +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/CODE_OF_CONDUCT.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/CONTRIBUTING.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/CONTRIBUTOR_REWARD.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/AsyncTranslate/AsyncTranslate.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/ILTranslator/ILTranslator.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/PDFCreation/PDFCreation.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/PDFParsing/PDFParsing.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/ParagraphFinding/ParagraphFinding.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/README.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/StylesAndFormulas/StylesAndFormulas.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/ImplementationDetails/Typesetting/Typesetting.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/README.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/deploy.sh +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/example/demo_glossary.csv +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-banner.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-big-logo-darkmode-with-transparent-background.svg +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-big-logo-with-transparent-background.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-big-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-big-logo.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-contributor_reward_example.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-preview.gif +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-small-logo-with-transparent-background.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-small-logo-with-transparent-background.svg +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/images/babeldoc-small-logo.png +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/index.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/intro-to-pdf-object.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/requirements.txt +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/docs/supported_languages.md +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/mkdocs.yml +0 -0
- {babeldoc-0.3.55 → babeldoc-0.3.57}/tests/test_translation_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BabelDOC
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.57
|
|
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
|
|
@@ -17,6 +17,7 @@ Requires-Dist: configargparse>=1.7
|
|
|
17
17
|
Requires-Dist: freetype-py>=2.5.1
|
|
18
18
|
Requires-Dist: httpx[socks]>=0.27.0
|
|
19
19
|
Requires-Dist: huggingface-hub>=0.27.0
|
|
20
|
+
Requires-Dist: hyperscan>=0.7.13
|
|
20
21
|
Requires-Dist: msgpack>=1.1.0
|
|
21
22
|
Requires-Dist: numpy>=2.0.2
|
|
22
23
|
Requires-Dist: onnx>=1.18.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.57"
|
|
@@ -781,10 +781,10 @@ class PDFCreater:
|
|
|
781
781
|
return False
|
|
782
782
|
|
|
783
783
|
def restore_media_box(self, doc: pymupdf.Document, mediabox_data: dict) -> None:
|
|
784
|
-
for
|
|
784
|
+
for xref, page_box_data in mediabox_data.items():
|
|
785
785
|
for name, box in page_box_data.items():
|
|
786
786
|
try:
|
|
787
|
-
doc.xref_set_key(
|
|
787
|
+
doc.xref_set_key(xref, name, box)
|
|
788
788
|
except Exception:
|
|
789
789
|
logger.debug(f"Error restoring media box {name} from PDF")
|
|
790
790
|
|
|
@@ -185,6 +185,7 @@ class AutomaticTermExtractor:
|
|
|
185
185
|
total_token_count = 0
|
|
186
186
|
for paragraph in page.pdf_paragraph:
|
|
187
187
|
if paragraph.debug_id is None or paragraph.unicode is None:
|
|
188
|
+
pbar.advance(1)
|
|
188
189
|
continue
|
|
189
190
|
if is_cid_paragraph(paragraph):
|
|
190
191
|
pbar.advance(1)
|
|
@@ -227,7 +228,6 @@ class AutomaticTermExtractor:
|
|
|
227
228
|
for u in inputs:
|
|
228
229
|
tracker.append_paragraph_unicode(u)
|
|
229
230
|
if not inputs:
|
|
230
|
-
pbar.advance(len(paragraphs.paragraphs))
|
|
231
231
|
return
|
|
232
232
|
prompt = LLM_PROMPT_TEMPLATE.format(
|
|
233
233
|
target_language=self.translation_config.lang_out,
|
|
@@ -254,8 +254,10 @@ class AutomaticTermExtractor:
|
|
|
254
254
|
)
|
|
255
255
|
|
|
256
256
|
except Exception as e:
|
|
257
|
-
logger.error(f"Error
|
|
257
|
+
logger.error(f"Error during automatic terms extract: {e}")
|
|
258
258
|
return
|
|
259
|
+
finally:
|
|
260
|
+
pbar.advance(len(paragraphs.paragraphs))
|
|
259
261
|
|
|
260
262
|
def procress(self, doc_il: ILDocument):
|
|
261
263
|
logger.info(f"{self.stage_name}: Starting term extraction for document.")
|
|
@@ -8,6 +8,7 @@ from abc import abstractmethod
|
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
import openai
|
|
11
|
+
from tenacity import before_sleep_log
|
|
11
12
|
from tenacity import retry
|
|
12
13
|
from tenacity import retry_if_exception_type
|
|
13
14
|
from tenacity import stop_after_attempt
|
|
@@ -198,7 +199,8 @@ class OpenAITranslator(BaseTranslator):
|
|
|
198
199
|
http_client=httpx.Client(
|
|
199
200
|
limits=httpx.Limits(
|
|
200
201
|
max_connections=None, max_keepalive_connections=None
|
|
201
|
-
)
|
|
202
|
+
),
|
|
203
|
+
timeout=60, # Set a reasonable timeout
|
|
202
204
|
),
|
|
203
205
|
)
|
|
204
206
|
self.add_cache_impact_parameters("temperature", self.options["temperature"])
|
|
@@ -213,10 +215,7 @@ class OpenAITranslator(BaseTranslator):
|
|
|
213
215
|
retry=retry_if_exception_type(openai.RateLimitError),
|
|
214
216
|
stop=stop_after_attempt(100),
|
|
215
217
|
wait=wait_exponential(multiplier=1, min=1, max=15),
|
|
216
|
-
before_sleep=
|
|
217
|
-
f"RateLimitError, retrying in {retry_state.next_action.sleep} seconds... "
|
|
218
|
-
f"(Attempt {retry_state.attempt_number}/100)"
|
|
219
|
-
),
|
|
218
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
220
219
|
)
|
|
221
220
|
def do_translate(self, text, rate_limit_params: dict = None) -> str:
|
|
222
221
|
response = self.client.chat.completions.create(
|
|
@@ -243,10 +242,7 @@ class OpenAITranslator(BaseTranslator):
|
|
|
243
242
|
retry=retry_if_exception_type(openai.RateLimitError),
|
|
244
243
|
stop=stop_after_attempt(100),
|
|
245
244
|
wait=wait_exponential(multiplier=1, min=1, max=15),
|
|
246
|
-
before_sleep=
|
|
247
|
-
f"RateLimitError, retrying in {retry_state.next_action.sleep} seconds... "
|
|
248
|
-
f"(Attempt {retry_state.attempt_number}/100)"
|
|
249
|
-
),
|
|
245
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
250
246
|
)
|
|
251
247
|
def do_llm_translate(self, text, rate_limit_params: dict = None):
|
|
252
248
|
if text is None:
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import io
|
|
3
|
+
import itertools
|
|
4
|
+
import logging
|
|
3
5
|
import re
|
|
6
|
+
import time
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
|
|
9
|
+
import hyperscan
|
|
10
|
+
import regex
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
6
14
|
|
|
7
15
|
class GlossaryEntry:
|
|
8
16
|
def __init__(self, source: str, target: str, target_language: str | None = None):
|
|
@@ -14,58 +22,92 @@ class GlossaryEntry:
|
|
|
14
22
|
return f"GlossaryEntry(source='{self.source}', target='{self.target}', target_language='{self.target_language}')"
|
|
15
23
|
|
|
16
24
|
|
|
25
|
+
def batched(iterable, n, *, strict=False):
|
|
26
|
+
# batched('ABCDEFG', 3) → ABC DEF G
|
|
27
|
+
if n < 1:
|
|
28
|
+
raise ValueError("n must be at least one")
|
|
29
|
+
iterator = iter(iterable)
|
|
30
|
+
while batch := tuple(itertools.islice(iterator, n)):
|
|
31
|
+
if strict and len(batch) != n:
|
|
32
|
+
raise ValueError("batched(): incomplete batch")
|
|
33
|
+
yield batch
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
TERM_NORM_PATTERN = re.compile(r"\s+", regex.UNICODE)
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
class Glossary:
|
|
18
40
|
def __init__(self, name: str, entries: list[GlossaryEntry]):
|
|
19
41
|
self.name = name
|
|
20
42
|
self.entries = entries
|
|
21
|
-
self.source_terms_regex: re.Pattern | None = None
|
|
22
43
|
self.normalized_lookup: dict[str, tuple[str, str]] = {}
|
|
44
|
+
self.id_lookup: list[tuple[str, str]] = []
|
|
45
|
+
self.hs_dbs: list[hyperscan.Database] | None = None
|
|
23
46
|
self._build_regex_and_lookup()
|
|
24
47
|
|
|
25
48
|
@staticmethod
|
|
26
49
|
def normalize_source(source_term: str) -> str:
|
|
27
50
|
"""Normalizes a source term by lowercasing and standardizing whitespace."""
|
|
28
51
|
term = source_term.lower()
|
|
29
|
-
term =
|
|
30
|
-
|
|
52
|
+
term = TERM_NORM_PATTERN.sub(
|
|
53
|
+
" ", term
|
|
31
54
|
) # Replace multiple whitespace with single space
|
|
32
55
|
return term.strip()
|
|
33
56
|
|
|
34
57
|
def _build_regex_and_lookup(self):
|
|
58
|
+
logger.debug(
|
|
59
|
+
f"start build regex for glossary {self.name} with {len(self.entries)} entries"
|
|
60
|
+
)
|
|
35
61
|
"""
|
|
36
62
|
Builds a combined regex for all source terms and a lookup dictionary
|
|
37
63
|
from normalized source terms to (original_source, original_target).
|
|
38
64
|
Regex patterns are sorted by length in descending order to prioritize longer matches.
|
|
39
65
|
"""
|
|
40
66
|
self.normalized_lookup = {}
|
|
41
|
-
regex_patterns_for_or: list[str] = []
|
|
42
67
|
|
|
43
68
|
if not self.entries:
|
|
44
69
|
self.source_terms_regex = None
|
|
45
70
|
return
|
|
46
71
|
|
|
47
|
-
|
|
72
|
+
self.hs_dbs = []
|
|
73
|
+
hs_pattern = []
|
|
74
|
+
start = time.time()
|
|
75
|
+
for idx, entry in enumerate(self.entries):
|
|
48
76
|
normalized_key = self.normalize_source(entry.source)
|
|
49
77
|
self.normalized_lookup[normalized_key] = (entry.source, entry.target)
|
|
78
|
+
self.id_lookup.append((entry.source, entry.target))
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# Sort patterns by length, longest first, to help with overlapping terms
|
|
60
|
-
regex_patterns_for_or.sort(key=len, reverse=True)
|
|
61
|
-
combined_regex_str = "|".join(
|
|
62
|
-
f"({pattern})" for pattern in regex_patterns_for_or
|
|
63
|
-
) # Add capturing groups
|
|
64
|
-
self.source_terms_regex = re.compile(
|
|
65
|
-
combined_regex_str, flags=re.IGNORECASE
|
|
80
|
+
hs_pattern.append((re.escape(entry.source).encode("utf-8"), idx))
|
|
81
|
+
|
|
82
|
+
chunk_size = 20000
|
|
83
|
+
for i, pattern_chunk in enumerate(
|
|
84
|
+
batched(hs_pattern, chunk_size, strict=False)
|
|
85
|
+
):
|
|
86
|
+
logger.debug(
|
|
87
|
+
f"building hs_db chunk {i + 1} / {len(self.entries) // chunk_size + 1}"
|
|
66
88
|
)
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
expressions, ids = zip(*pattern_chunk, strict=False)
|
|
90
|
+
|
|
91
|
+
hs_db = hyperscan.Database()
|
|
92
|
+
hs_db.compile(
|
|
93
|
+
expressions=expressions,
|
|
94
|
+
ids=ids,
|
|
95
|
+
elements=len(pattern_chunk),
|
|
96
|
+
flags=hyperscan.HS_FLAG_CASELESS | hyperscan.HS_FLAG_SINGLEMATCH,
|
|
97
|
+
# | hyperscan.HS_FLAG_UTF8
|
|
98
|
+
# | hyperscan.HS_FLAG_UCP,
|
|
99
|
+
)
|
|
100
|
+
self.hs_dbs.append(hs_db)
|
|
101
|
+
|
|
102
|
+
end = time.time()
|
|
103
|
+
logger.debug(
|
|
104
|
+
f"finished building regex for glossary {self.name} in {end - start:.2f} seconds"
|
|
105
|
+
)
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"build hs database for glossary {self.name} with {len(self.entries)} entries, hs_info: {self.hs_dbs[0].info()}"
|
|
108
|
+
)
|
|
109
|
+
if not self.hs_dbs:
|
|
110
|
+
self.hs_dbs = None
|
|
69
111
|
|
|
70
112
|
@classmethod
|
|
71
113
|
def from_csv(cls, file_path: Path, target_lang_out: str) -> "Glossary":
|
|
@@ -136,24 +178,23 @@ class Glossary:
|
|
|
136
178
|
|
|
137
179
|
def get_active_entries_for_text(self, text: str) -> list[tuple[str, str]]:
|
|
138
180
|
"""Returns a list of (original_source, target_text) tuples for terms found in the given text."""
|
|
139
|
-
if not self.
|
|
181
|
+
if not self.hs_dbs or not text:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
text = TERM_NORM_PATTERN.sub(" ", text) # Normalize whitespace in the text
|
|
185
|
+
if not text:
|
|
140
186
|
return []
|
|
141
187
|
|
|
142
188
|
active_entries = []
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if normalized_frag in self.normalized_lookup:
|
|
155
|
-
original_source, target_text = self.normalized_lookup[normalized_frag]
|
|
156
|
-
if original_source not in unique_added_original_sources:
|
|
157
|
-
active_entries.append((original_source, target_text))
|
|
158
|
-
unique_added_original_sources.add(original_source)
|
|
189
|
+
|
|
190
|
+
def on_match(
|
|
191
|
+
idx: int, _from: int, _to: int, _flags: int, _context=None
|
|
192
|
+
) -> bool | None:
|
|
193
|
+
active_entries.append(self.id_lookup[idx])
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
for hs_db in self.hs_dbs:
|
|
197
|
+
# Scan the text with the hyperscan database
|
|
198
|
+
scratch = hyperscan.Scratch(hs_db)
|
|
199
|
+
hs_db.scan(text.encode("utf-8"), on_match, scratch=scratch)
|
|
159
200
|
return active_entries
|
|
@@ -425,6 +425,8 @@ def fix_null_xref(doc: Document) -> None:
|
|
|
425
425
|
elif obj and "/LZWDecode" in obj:
|
|
426
426
|
data = doc.xref_stream(i)
|
|
427
427
|
doc.update_stream(i, data)
|
|
428
|
+
elif obj and "/Annots" in obj:
|
|
429
|
+
doc.xref_set_key(i, "Annots", "null")
|
|
428
430
|
except Exception:
|
|
429
431
|
doc.update_object(i, "[]")
|
|
430
432
|
|
|
@@ -625,28 +627,22 @@ def do_translate(
|
|
|
625
627
|
|
|
626
628
|
def fix_media_box(doc: Document) -> None:
|
|
627
629
|
mediabox_data = {}
|
|
628
|
-
for
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if
|
|
632
|
-
mediabox = (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
x1 = page.cropbox.x1
|
|
645
|
-
y1 = page.cropbox.y1
|
|
646
|
-
page_box_data["CropBox"] = doc.xref_get_key(page.xref, "CropBox")[1]
|
|
647
|
-
doc.xref_set_key(page.xref, "CropBox", f"[0 0 {x1 - x0} {y1 - x0}]")
|
|
648
|
-
if page_box_data:
|
|
649
|
-
mediabox_data[page.number] = page_box_data
|
|
630
|
+
for x in range(1, doc.xref_length()):
|
|
631
|
+
t = doc.xref_get_key(x, "Type")
|
|
632
|
+
box_set = {}
|
|
633
|
+
if t[1] in ["/Pages", "/Page"]:
|
|
634
|
+
mediabox = doc.xref_get_key(x, "MediaBox")
|
|
635
|
+
if mediabox[0] != "null":
|
|
636
|
+
_, _, x1, y1 = mediabox[1].replace("[", "").replace("]", "").split(" ")
|
|
637
|
+
doc.xref_set_key(x, "MediaBox", f"[0 0 {x1} {y1}]")
|
|
638
|
+
box_set["MediaBox"] = mediabox[1]
|
|
639
|
+
for k in ["CropBox", "BleedBox", "TrimBox", "ArtBox"]:
|
|
640
|
+
box = doc.xref_get_key(x, k)
|
|
641
|
+
if box[0] != "null":
|
|
642
|
+
box_set[k] = box[1]
|
|
643
|
+
doc.xref_set_key(x, k, "null")
|
|
644
|
+
if box_set:
|
|
645
|
+
mediabox_data[x] = box_set
|
|
650
646
|
return mediabox_data
|
|
651
647
|
|
|
652
648
|
|
|
@@ -45,50 +45,85 @@ class ResultMerger:
|
|
|
45
45
|
merged_dual_path = None
|
|
46
46
|
merged_no_watermark_mono_path = None
|
|
47
47
|
merged_no_watermark_dual_path = None
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# Merge dual-language PDFs if they exist
|
|
58
|
-
if any(r.dual_pdf_path for r in results.values()):
|
|
59
|
-
merged_dual_path = self._merge_pdfs(
|
|
60
|
-
[r.dual_pdf_path for r in sorted_results.values() if r.dual_pdf_path],
|
|
61
|
-
dual_file_name,
|
|
62
|
-
tag="merged_dual",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
if any(
|
|
66
|
-
r.dual_pdf_path != r.no_watermark_dual_pdf_path
|
|
67
|
-
or r.mono_pdf_path != r.no_watermark_mono_pdf_path
|
|
68
|
-
for r in results.values()
|
|
69
|
-
):
|
|
70
|
-
# Merge no-watermark PDFs if they exist
|
|
71
|
-
if any(r.no_watermark_mono_pdf_path for r in results.values()):
|
|
72
|
-
merged_no_watermark_mono_path = self._merge_pdfs(
|
|
48
|
+
try:
|
|
49
|
+
# Merge monolingual PDFs if they exist
|
|
50
|
+
if (
|
|
51
|
+
any(r.mono_pdf_path for r in results.values())
|
|
52
|
+
and not self.config.no_mono
|
|
53
|
+
):
|
|
54
|
+
merged_mono_path = self._merge_pdfs(
|
|
73
55
|
[
|
|
74
|
-
r.
|
|
56
|
+
r.mono_pdf_path
|
|
75
57
|
for r in sorted_results.values()
|
|
76
|
-
if r.
|
|
58
|
+
if r.mono_pdf_path
|
|
77
59
|
],
|
|
78
|
-
|
|
79
|
-
tag="
|
|
60
|
+
mono_file_name,
|
|
61
|
+
tag="merged_mono",
|
|
80
62
|
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Error merging monolingual PDFs: {e}")
|
|
65
|
+
merged_mono_path = None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Merge dual-language PDFs if they exist
|
|
69
|
+
if (
|
|
70
|
+
any(r.dual_pdf_path for r in results.values())
|
|
71
|
+
and not self.config.no_dual
|
|
72
|
+
):
|
|
73
|
+
merged_dual_path = self._merge_pdfs(
|
|
84
74
|
[
|
|
85
|
-
r.
|
|
75
|
+
r.dual_pdf_path
|
|
86
76
|
for r in sorted_results.values()
|
|
87
|
-
if r.
|
|
77
|
+
if r.dual_pdf_path
|
|
88
78
|
],
|
|
89
|
-
|
|
90
|
-
tag="
|
|
79
|
+
dual_file_name,
|
|
80
|
+
tag="merged_dual",
|
|
91
81
|
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Error merging dual-language PDFs: {e}")
|
|
84
|
+
merged_dual_path = None
|
|
85
|
+
|
|
86
|
+
if any(
|
|
87
|
+
r.dual_pdf_path != r.no_watermark_dual_pdf_path
|
|
88
|
+
or r.mono_pdf_path != r.no_watermark_mono_pdf_path
|
|
89
|
+
for r in results.values()
|
|
90
|
+
):
|
|
91
|
+
try:
|
|
92
|
+
# Merge no-watermark PDFs if they exist
|
|
93
|
+
if (
|
|
94
|
+
any(r.no_watermark_mono_pdf_path for r in results.values())
|
|
95
|
+
and not self.config.no_mono
|
|
96
|
+
):
|
|
97
|
+
merged_no_watermark_mono_path = self._merge_pdfs(
|
|
98
|
+
[
|
|
99
|
+
r.no_watermark_mono_pdf_path
|
|
100
|
+
for r in sorted_results.values()
|
|
101
|
+
if r.no_watermark_mono_pdf_path
|
|
102
|
+
],
|
|
103
|
+
mono_file_name_no_watermark,
|
|
104
|
+
tag="merged_no_watermark_mono",
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error merging no-watermark PDFs: {e}")
|
|
108
|
+
merged_no_watermark_mono_path = None
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if (
|
|
112
|
+
any(r.no_watermark_dual_pdf_path for r in results.values())
|
|
113
|
+
and not self.config.no_dual
|
|
114
|
+
):
|
|
115
|
+
merged_no_watermark_dual_path = self._merge_pdfs(
|
|
116
|
+
[
|
|
117
|
+
r.no_watermark_dual_pdf_path
|
|
118
|
+
for r in sorted_results.values()
|
|
119
|
+
if r.no_watermark_dual_pdf_path
|
|
120
|
+
],
|
|
121
|
+
"merged_no_watermark_dual.pdf",
|
|
122
|
+
tag="merged_no_watermark_dual",
|
|
123
|
+
)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"Error merging no-watermark PDFs: {e}")
|
|
126
|
+
merged_no_watermark_dual_path = None
|
|
92
127
|
|
|
93
128
|
# Create merged result
|
|
94
129
|
merged_result = TranslateResult(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "BabelDOC"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.57"
|
|
4
4
|
description = "Yet Another Document Translator"
|
|
5
5
|
license = "AGPL-3.0"
|
|
6
6
|
readme = "README.md"
|
|
@@ -44,6 +44,7 @@ dependencies = [
|
|
|
44
44
|
"opencv-python-headless>=4.10.0.84",
|
|
45
45
|
"rapidocr-onnxruntime>=1.4.4",
|
|
46
46
|
"pyzstd>=0.17.0",
|
|
47
|
+
"hyperscan>=0.7.13",
|
|
47
48
|
]
|
|
48
49
|
|
|
49
50
|
[project.optional-dependencies]
|
|
@@ -150,7 +151,7 @@ pythonpath = [".", "src"]
|
|
|
150
151
|
testpaths = ["tests"]
|
|
151
152
|
|
|
152
153
|
[bumpver]
|
|
153
|
-
current_version = "0.3.
|
|
154
|
+
current_version = "0.3.57"
|
|
154
155
|
version_pattern = "MAJOR.MINOR.PATCH[.PYTAGNUM]"
|
|
155
156
|
|
|
156
157
|
[bumpver.file_patterns]
|