BabelDOC 0.1.6__py3-none-any.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 (42) hide show
  1. babeldoc/__init__.py +1 -0
  2. babeldoc/asynchronize/__init__.py +51 -0
  3. babeldoc/const.py +7 -0
  4. babeldoc/converter.py +510 -0
  5. babeldoc/document_il/__init__.py +43 -0
  6. babeldoc/document_il/backend/__init__.py +0 -0
  7. babeldoc/document_il/backend/pdf_creater.py +402 -0
  8. babeldoc/document_il/frontend/__init__.py +0 -0
  9. babeldoc/document_il/frontend/il_creater.py +348 -0
  10. babeldoc/document_il/il_version_1.py +799 -0
  11. babeldoc/document_il/il_version_1.rnc +141 -0
  12. babeldoc/document_il/il_version_1.rng +390 -0
  13. babeldoc/document_il/il_version_1.xsd +235 -0
  14. babeldoc/document_il/midend/__init__.py +0 -0
  15. babeldoc/document_il/midend/add_debug_information.py +138 -0
  16. babeldoc/document_il/midend/il_translator.py +553 -0
  17. babeldoc/document_il/midend/layout_parser.py +180 -0
  18. babeldoc/document_il/midend/paragraph_finder.py +452 -0
  19. babeldoc/document_il/midend/remove_descent.py +168 -0
  20. babeldoc/document_il/midend/styles_and_formulas.py +701 -0
  21. babeldoc/document_il/midend/typesetting.py +761 -0
  22. babeldoc/document_il/translator/__init__.py +0 -0
  23. babeldoc/document_il/translator/cache.py +145 -0
  24. babeldoc/document_il/translator/translator.py +267 -0
  25. babeldoc/document_il/utils/fontmap.py +227 -0
  26. babeldoc/document_il/utils/layout_helper.py +467 -0
  27. babeldoc/document_il/utils/style_helper.py +86 -0
  28. babeldoc/document_il/xml_converter.py +51 -0
  29. babeldoc/docvision/README.md +0 -0
  30. babeldoc/docvision/__init__.py +0 -0
  31. babeldoc/docvision/doclayout.py +278 -0
  32. babeldoc/docvision/rpc_doclayout.py +162 -0
  33. babeldoc/high_level.py +468 -0
  34. babeldoc/main.py +466 -0
  35. babeldoc/pdfinterp.py +464 -0
  36. babeldoc/progress_monitor.py +146 -0
  37. babeldoc/translation_config.py +156 -0
  38. babeldoc-0.1.6.dist-info/METADATA +283 -0
  39. babeldoc-0.1.6.dist-info/RECORD +42 -0
  40. babeldoc-0.1.6.dist-info/WHEEL +4 -0
  41. babeldoc-0.1.6.dist-info/entry_points.txt +2 -0
  42. babeldoc-0.1.6.dist-info/licenses/LICENSE +661 -0
babeldoc/main.py ADDED
@@ -0,0 +1,466 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ import configargparse
6
+ import tqdm
7
+ from rich.progress import BarColumn
8
+ from rich.progress import MofNCompleteColumn
9
+ from rich.progress import Progress
10
+ from rich.progress import TextColumn
11
+ from rich.progress import TimeElapsedColumn
12
+ from rich.progress import TimeRemainingColumn
13
+
14
+ import babeldoc.high_level
15
+ from babeldoc.const import get_cache_file_path
16
+ from babeldoc.document_il.translator.translator import BingTranslator
17
+ from babeldoc.document_il.translator.translator import GoogleTranslator
18
+ from babeldoc.document_il.translator.translator import OpenAITranslator
19
+ from babeldoc.document_il.translator.translator import set_translate_rate_limiter
20
+ from babeldoc.translation_config import TranslationConfig
21
+
22
+ logger = logging.getLogger(__name__)
23
+ __version__ = "0.1.6"
24
+
25
+
26
+ def create_parser():
27
+ parser = configargparse.ArgParser(
28
+ config_file_parser_class=configargparse.TomlConfigParser(["babeldoc"]),
29
+ )
30
+ parser.add_argument(
31
+ "-c",
32
+ "--my-config",
33
+ required=False,
34
+ is_config_file=True,
35
+ help="config file path",
36
+ )
37
+ parser.add_argument(
38
+ "--version",
39
+ action="version",
40
+ version=f"%(prog)s {__version__}",
41
+ )
42
+ parser.add_argument(
43
+ "--files",
44
+ type=str,
45
+ # nargs="*",
46
+ action="append",
47
+ help="One or more paths to PDF files.",
48
+ )
49
+ parser.add_argument(
50
+ "--debug",
51
+ "-d",
52
+ default=False,
53
+ action="store_true",
54
+ help="Use debug logging level.",
55
+ )
56
+ parser.add_argument(
57
+ "--warmup",
58
+ default=False,
59
+ action="store_true",
60
+ help="Only download and verify required assets then exit.",
61
+ )
62
+ parser.add_argument(
63
+ "--rpc-doclayout",
64
+ default=None,
65
+ help="RPC service host address for document layout analysis",
66
+ type=str,
67
+ )
68
+ translation_params = parser.add_argument_group(
69
+ "Translation",
70
+ description="Used during translation",
71
+ )
72
+ translation_params.add_argument(
73
+ "--pages",
74
+ "-p",
75
+ type=str,
76
+ help="Pages to translate. If not set, translate all pages. like: 1,2,1-,-3,3-5",
77
+ )
78
+ translation_params.add_argument(
79
+ "--lang-in",
80
+ "-li",
81
+ type=str,
82
+ default="en",
83
+ help="The code of source language.",
84
+ )
85
+ translation_params.add_argument(
86
+ "--lang-out",
87
+ "-lo",
88
+ type=str,
89
+ default="zh",
90
+ help="The code of target language.",
91
+ )
92
+ translation_params.add_argument(
93
+ "--output",
94
+ "-o",
95
+ type=str,
96
+ default=None,
97
+ help="Output directory for files. if not set, use same as input.",
98
+ )
99
+ translation_params.add_argument(
100
+ "--qps",
101
+ "-q",
102
+ type=int,
103
+ default=4,
104
+ help="QPS limit of translation service",
105
+ )
106
+ translation_params.add_argument(
107
+ "--ignore-cache",
108
+ "-ic",
109
+ default=False,
110
+ action="store_true",
111
+ help="Ignore translation cache.",
112
+ )
113
+ translation_params.add_argument(
114
+ "--no-dual",
115
+ default=False,
116
+ action="store_true",
117
+ help="Do not output bilingual PDF files",
118
+ )
119
+ translation_params.add_argument(
120
+ "--no-mono",
121
+ default=False,
122
+ action="store_true",
123
+ help="Do not output monolingual PDF files",
124
+ )
125
+ translation_params.add_argument(
126
+ "--formular-font-pattern",
127
+ type=str,
128
+ default=None,
129
+ help="Font pattern to identify formula text",
130
+ )
131
+ translation_params.add_argument(
132
+ "--formular-char-pattern",
133
+ type=str,
134
+ default=None,
135
+ help="Character pattern to identify formula text",
136
+ )
137
+ translation_params.add_argument(
138
+ "--split-short-lines",
139
+ default=False,
140
+ action="store_true",
141
+ help="Force split short lines into different paragraphs (may cause poor typesetting & bugs)",
142
+ )
143
+ translation_params.add_argument(
144
+ "--short-line-split-factor",
145
+ type=float,
146
+ default=0.8,
147
+ help="Split threshold factor. The actual threshold is the median length of all lines on the current page * this factor",
148
+ )
149
+ translation_params.add_argument(
150
+ "--skip-clean",
151
+ default=False,
152
+ action="store_true",
153
+ help="Skip PDF cleaning step",
154
+ )
155
+ translation_params.add_argument(
156
+ "--dual-translate-first",
157
+ default=False,
158
+ action="store_true",
159
+ help="Put translated pages first in dual PDF mode",
160
+ )
161
+ service_params = translation_params.add_mutually_exclusive_group()
162
+ service_params.add_argument(
163
+ "--openai",
164
+ default=False,
165
+ action="store_true",
166
+ help="Use OpenAI translator.",
167
+ )
168
+ service_params.add_argument(
169
+ "--google",
170
+ default=False,
171
+ action="store_true",
172
+ help="Use Google translator.",
173
+ )
174
+ service_params.add_argument(
175
+ "--bing",
176
+ default=False,
177
+ action="store_true",
178
+ help="Use Bing translator.",
179
+ )
180
+ openai_params = parser.add_argument_group(
181
+ "Translation - OpenAI Options",
182
+ description="OpenAI specific options",
183
+ )
184
+ openai_params.add_argument(
185
+ "--openai-model",
186
+ "-m",
187
+ type=str,
188
+ default="gpt-4o-mini",
189
+ help="The OpenAI model to use for translation.",
190
+ )
191
+ openai_params.add_argument(
192
+ "--openai-base-url",
193
+ "-b",
194
+ type=str,
195
+ default=None,
196
+ help="The base URL for the OpenAI API.",
197
+ )
198
+ openai_params.add_argument(
199
+ "--openai-api-key",
200
+ "-k",
201
+ type=str,
202
+ default=None,
203
+ help="The API key for the OpenAI API.",
204
+ )
205
+
206
+ return parser
207
+
208
+
209
+ def create_progress_handler(translation_config: TranslationConfig):
210
+ """Create a progress handler function based on the configuration.
211
+
212
+ Args:
213
+ translation_config: The translation configuration.
214
+
215
+ Returns:
216
+ A tuple of (progress_context, progress_handler), where progress_context is a context
217
+ manager that should be used to wrap the translation process, and progress_handler
218
+ is a function that will be called with progress events.
219
+ """
220
+ if translation_config.use_rich_pbar:
221
+ progress = Progress(
222
+ TextColumn("[progress.description]{task.description}"),
223
+ BarColumn(),
224
+ MofNCompleteColumn(),
225
+ TimeElapsedColumn(),
226
+ TimeRemainingColumn(),
227
+ )
228
+ translate_task_id = progress.add_task("translate", total=100)
229
+ stage_tasks = {}
230
+
231
+ def progress_handler(event):
232
+ if event["type"] == "progress_start":
233
+ stage_tasks[event["stage"]] = progress.add_task(
234
+ f"{event['stage']}",
235
+ total=event.get("stage_total", 100),
236
+ )
237
+ elif event["type"] == "progress_update":
238
+ stage = event["stage"]
239
+ if stage in stage_tasks:
240
+ progress.update(
241
+ stage_tasks[stage],
242
+ completed=event["stage_current"],
243
+ total=event["stage_total"],
244
+ description=f"{event['stage']} ({event['stage_current']}/{event['stage_total']})",
245
+ refresh=True,
246
+ )
247
+ progress.update(
248
+ translate_task_id,
249
+ completed=event["overall_progress"],
250
+ refresh=True,
251
+ )
252
+ elif event["type"] == "progress_end":
253
+ stage = event["stage"]
254
+ if stage in stage_tasks:
255
+ progress.update(
256
+ stage_tasks[stage],
257
+ completed=event["stage_total"],
258
+ total=event["stage_total"],
259
+ description=f"{event['stage']} (Complete)",
260
+ refresh=True,
261
+ )
262
+ progress.update(
263
+ translate_task_id,
264
+ completed=event["overall_progress"],
265
+ refresh=True,
266
+ )
267
+ progress.refresh()
268
+
269
+ return progress, progress_handler
270
+ else:
271
+ pbar = tqdm.tqdm(total=100, desc="translate")
272
+
273
+ def progress_handler(event):
274
+ if event["type"] == "progress_update":
275
+ pbar.update(event["overall_progress"] - pbar.n)
276
+ pbar.set_description(
277
+ f"{event['stage']} ({event['stage_current']}/{event['stage_total']})",
278
+ )
279
+ elif event["type"] == "progress_end":
280
+ pbar.set_description(f"{event['stage']} (Complete)")
281
+ pbar.refresh()
282
+
283
+ return pbar, progress_handler
284
+
285
+
286
+ async def main():
287
+ parser = create_parser()
288
+ args = parser.parse_args()
289
+
290
+ if args.debug:
291
+ logging.getLogger().setLevel(logging.DEBUG)
292
+
293
+ if args.warmup:
294
+ logger.info("Warmup completed, exiting...")
295
+ return
296
+
297
+ # 验证翻译服务选择
298
+ if not (args.openai or args.google or args.bing):
299
+ parser.error("必须选择一个翻译服务:--openai、--google 或 --bing")
300
+
301
+ # 验证 OpenAI 参数
302
+ if args.openai and not args.openai_api_key:
303
+ parser.error("使用 OpenAI 服务时必须提供 API key")
304
+
305
+ # 实例化翻译器
306
+ if args.openai:
307
+ translator = OpenAITranslator(
308
+ lang_in=args.lang_in,
309
+ lang_out=args.lang_out,
310
+ model=args.openai_model,
311
+ base_url=args.openai_base_url,
312
+ api_key=args.openai_api_key,
313
+ ignore_cache=args.ignore_cache,
314
+ )
315
+ elif args.bing:
316
+ translator = BingTranslator(
317
+ lang_in=args.lang_in,
318
+ lang_out=args.lang_out,
319
+ ignore_cache=args.ignore_cache,
320
+ )
321
+ else:
322
+ translator = GoogleTranslator(
323
+ lang_in=args.lang_in,
324
+ lang_out=args.lang_out,
325
+ ignore_cache=args.ignore_cache,
326
+ )
327
+
328
+ # 设置翻译速率限制
329
+ set_translate_rate_limiter(args.qps)
330
+
331
+ # 初始化文档布局模型
332
+ if args.rpc_doclayout:
333
+ from babeldoc.docvision.rpc_doclayout import RpcDocLayoutModel
334
+
335
+ doc_layout_model = RpcDocLayoutModel(host=args.rpc_doclayout)
336
+ else:
337
+ from babeldoc.docvision.doclayout import DocLayoutModel
338
+
339
+ doc_layout_model = DocLayoutModel.load_onnx()
340
+
341
+ pending_files = []
342
+ for file in args.files:
343
+ # 清理文件路径,去除两端的引号
344
+ if file.startswith("--files="):
345
+ file = file[len("--files=") :]
346
+ file = file.lstrip("-").strip("\"'")
347
+ if not Path(file).exists():
348
+ logger.error(f"文件不存在:{file}")
349
+ exit(1)
350
+ if not file.endswith(".pdf"):
351
+ logger.error(f"文件不是 PDF 文件:{file}")
352
+ exit(1)
353
+ pending_files.append(file)
354
+
355
+ font_path = get_cache_file_path("source-han-serif-cn.ttf")
356
+
357
+ # 验证字体
358
+ if font_path:
359
+ if not Path(font_path).exists():
360
+ logger.error(f"字体文件不存在:{font_path}")
361
+ exit(1)
362
+ if not str(font_path).endswith(".ttf"):
363
+ logger.error(f"字体文件不是 TTF 文件:{font_path}")
364
+ exit(1)
365
+
366
+ if args.output:
367
+ if not Path(args.output).exists():
368
+ logger.info(f"输出目录不存在,创建:{args.output}")
369
+ try:
370
+ Path(args.output).mkdir(parents=True, exist_ok=True)
371
+ except OSError:
372
+ logger.critical(
373
+ f"Failed to create output folder at {args.output}",
374
+ exc_info=True,
375
+ )
376
+ exit(1)
377
+ else:
378
+ args.output = None
379
+
380
+ for file in pending_files:
381
+ # 清理文件路径,去除两端的引号
382
+ file = file.strip("\"'")
383
+ # 创建配置对象
384
+ config = TranslationConfig(
385
+ input_file=file,
386
+ font=font_path,
387
+ pages=args.pages,
388
+ output_dir=args.output,
389
+ translator=translator,
390
+ debug=args.debug,
391
+ lang_in=args.lang_in,
392
+ lang_out=args.lang_out,
393
+ no_dual=args.no_dual,
394
+ no_mono=args.no_mono,
395
+ qps=args.qps,
396
+ formular_font_pattern=args.formular_font_pattern,
397
+ formular_char_pattern=args.formular_char_pattern,
398
+ split_short_lines=args.split_short_lines,
399
+ short_line_split_factor=args.short_line_split_factor,
400
+ doc_layout_model=doc_layout_model,
401
+ skip_clean=args.skip_clean,
402
+ dual_translate_first=args.dual_translate_first,
403
+ )
404
+
405
+ # Create progress handler
406
+ progress_context, progress_handler = create_progress_handler(config)
407
+
408
+ # 开始翻译
409
+ with progress_context:
410
+ async for event in babeldoc.high_level.async_translate(config):
411
+ progress_handler(event)
412
+ if config.debug:
413
+ logger.debug(event)
414
+ if event["type"] == "finish":
415
+ result = event["translate_result"]
416
+ logger.info("Translation Result:")
417
+ logger.info(f" Original PDF: {result.original_pdf_path}")
418
+ logger.info(f" Time Cost: {result.total_seconds:.2f}s")
419
+ logger.info(f" Mono PDF: {result.mono_pdf_path or 'None'}")
420
+ logger.info(f" Dual PDF: {result.dual_pdf_path or 'None'}")
421
+ break
422
+
423
+
424
+ # for backward compatibility
425
+ def create_cache_folder():
426
+ return babeldoc.high_level.create_cache_folder()
427
+
428
+
429
+ # for backward compatibility
430
+ def download_font_assets():
431
+ return babeldoc.high_level.download_font_assets()
432
+
433
+
434
+ def cli():
435
+ """Command line interface entry point."""
436
+ from rich.logging import RichHandler
437
+
438
+ logging.basicConfig(level=logging.INFO, handlers=[RichHandler()])
439
+
440
+ logging.getLogger("httpx").setLevel("CRITICAL")
441
+ logging.getLogger("httpx").propagate = False
442
+ logging.getLogger("openai").setLevel("CRITICAL")
443
+ logging.getLogger("openai").propagate = False
444
+ logging.getLogger("httpcore").setLevel("CRITICAL")
445
+ logging.getLogger("httpcore").propagate = False
446
+ logging.getLogger("http11").setLevel("CRITICAL")
447
+ logging.getLogger("http11").propagate = False
448
+ for v in logging.Logger.manager.loggerDict.values():
449
+ if getattr(v, "name", None) is None:
450
+ continue
451
+ if (
452
+ v.name.startswith("pdfminer")
453
+ or v.name.startswith("peewee")
454
+ or v.name.startswith("httpx")
455
+ or "http11" in v.name
456
+ or "openai" in v.name
457
+ ):
458
+ v.disabled = True
459
+ v.propagate = False
460
+
461
+ babeldoc.high_level.init()
462
+ asyncio.run(main())
463
+
464
+
465
+ if __name__ == "__main__":
466
+ cli()