multimodalart HF Staff commited on
Commit
df62d3c
·
verified ·
1 Parent(s): 0f5ba4a

Remove unused modules/assets from earlier upload

Browse files
Files changed (38) hide show
  1. NaviOCR/data_reader_writer/__init__.py +0 -10
  2. NaviOCR/data_reader_writer/base.py +0 -62
  3. NaviOCR/data_reader_writer/filebase.py +0 -62
  4. NaviOCR/data_reader_writer/imagefile.py +0 -66
  5. NaviOCR/engine.py +0 -161
  6. NaviOCR/src/__init__.py +0 -1
  7. NaviOCR/src/model_output_to_middle_json.py +0 -37
  8. NaviOCR/src/vlm_analyze.py +0 -63
  9. NaviOCR/src/vlm_magic_model.py +0 -561
  10. NaviOCR/src/vlm_middle_json_mkcontent.py +0 -176
  11. NaviOCR/tools/boxbase.py +0 -224
  12. NaviOCR/tools/char_utils.py +0 -55
  13. NaviOCR/tools/check_sys_env.py +0 -38
  14. NaviOCR/tools/config_reader.py +0 -136
  15. NaviOCR/tools/cut_image.py +0 -36
  16. NaviOCR/tools/draw_bbox.py +0 -634
  17. NaviOCR/tools/enum_class.py +0 -67
  18. NaviOCR/tools/guess_suffix_or_lang.py +0 -55
  19. NaviOCR/tools/hash_utils.py +0 -30
  20. NaviOCR/tools/language.py +0 -48
  21. NaviOCR/tools/magic_model_utils.py +0 -251
  22. NaviOCR/tools/model_utils.py +0 -462
  23. NaviOCR/tools/os_env_config.py +0 -30
  24. NaviOCR/tools/pdf_image_tools.py +0 -26
  25. NaviOCR/tools/pdf_image_tools_PyMuPDF.py +0 -289
  26. NaviOCR/tools/pdf_image_tools_pdfium.py +0 -268
  27. NaviOCR/tools/pdf_page_id.py +0 -10
  28. NaviOCR/tools/pdf_reader.py +0 -111
  29. NaviOCR/tools/read_file.py +0 -21
  30. NaviOCR/version.py +0 -1
  31. NaviOCR/vlm_utils/NaviOCR_model.py +0 -126
  32. NaviOCR/vlm_utils/version.py +0 -1
  33. NaviOCR/vlm_utils/vlm_client/vllm_async_engine_client.py +0 -259
  34. NaviOCR/vlm_utils/vlm_client/vllm_engine_client.py +0 -255
  35. NaviOCR/vlm_utils/vlm_client/vllm_v1_no_repeat_ngram.py +0 -93
  36. examples/layout.jpg +0 -3
  37. examples/layout_distorted.jpg +0 -3
  38. examples/text.png +0 -0
NaviOCR/data_reader_writer/__init__.py DELETED
@@ -1,10 +0,0 @@
1
- from .base import DataReader, DataWriter
2
- from .filebase import FileBasedDataReader, FileBasedDataWriter
3
- from .imagefile import ImageDataWriter
4
- __all__ = [
5
- "DataReader",
6
- "DataWriter",
7
- "FileBasedDataReader",
8
- "FileBasedDataWriter",
9
- "ImageDataWriter",
10
- ]
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/data_reader_writer/base.py DELETED
@@ -1,62 +0,0 @@
1
-
2
- from abc import ABC, abstractmethod
3
-
4
-
5
- class DataReader(ABC):
6
-
7
- def read(self, path: str) -> bytes:
8
- """Read the file.
9
-
10
- Args:
11
- path (str): file path to read
12
-
13
- Returns:
14
- bytes: the content of the file
15
- """
16
- return self.read_at(path)
17
-
18
- @abstractmethod
19
- def read_at(self, path: str, offset: int = 0, limit: int = -1) -> bytes:
20
- """Read the file at offset and limit.
21
-
22
- Args:
23
- path (str): the file path
24
- offset (int, optional): the number of bytes skipped. Defaults to 0.
25
- limit (int, optional): the length of bytes want to read. Defaults to -1.
26
-
27
- Returns:
28
- bytes: the content of the file
29
- """
30
- pass
31
-
32
-
33
- class DataWriter(ABC):
34
- @abstractmethod
35
- def write(self, path: str, data: bytes) -> None:
36
- """Write the data to the file.
37
-
38
- Args:
39
- path (str): the target file where to write
40
- data (bytes): the data want to write
41
- """
42
- pass
43
-
44
- def write_string(self, path: str, data: str) -> None:
45
- """Write the data to file, the data will be encoded to bytes.
46
-
47
- Args:
48
- path (str): the target file where to write
49
- data (str): the data want to write
50
- """
51
-
52
- def safe_encode(data: str, method: str):
53
- try:
54
- bit_data = data.encode(encoding=method, errors='replace')
55
- return bit_data, True
56
- except: # noqa
57
- return None, False
58
- for method in ['utf-8', 'ascii']:
59
- bit_data, flag = safe_encode(data, method)
60
- if flag:
61
- self.write(path, bit_data)
62
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/data_reader_writer/filebase.py DELETED
@@ -1,62 +0,0 @@
1
- import os
2
-
3
- from .base import DataReader, DataWriter
4
-
5
-
6
- class FileBasedDataReader(DataReader):
7
- def __init__(self, parent_dir: str = ''):
8
- """Initialized with parent_dir.
9
-
10
- Args:
11
- parent_dir (str, optional): the parent directory that may be used within methods. Defaults to ''.
12
- """
13
- self._parent_dir = parent_dir
14
-
15
- def read_at(self, path: str, offset: int = 0, limit: int = -1) -> bytes:
16
- """Read at offset and limit.
17
-
18
- Args:
19
- path (str): the path of file, if the path is relative path, it will be joined with parent_dir.
20
- offset (int, optional): the number of bytes skipped. Defaults to 0.
21
- limit (int, optional): the length of bytes want to read. Defaults to -1.
22
-
23
- Returns:
24
- bytes: the content of file
25
- """
26
- fn_path = path
27
- if not os.path.isabs(fn_path) and len(self._parent_dir) > 0:
28
- fn_path = os.path.join(self._parent_dir, path)
29
-
30
- with open(fn_path, 'rb') as f:
31
- f.seek(offset)
32
- if limit == -1:
33
- return f.read()
34
- else:
35
- return f.read(limit)
36
-
37
-
38
- class FileBasedDataWriter(DataWriter):
39
- def __init__(self, parent_dir: str = '') -> None:
40
- """Initialized with parent_dir.
41
-
42
- Args:
43
- parent_dir (str, optional): the parent directory that may be used within methods. Defaults to ''.
44
- """
45
- self._parent_dir = parent_dir
46
-
47
- def write(self, path: str, data: bytes) -> None:
48
- """Write file with data.
49
-
50
- Args:
51
- path (str): the path of file, if the path is relative path, it will be joined with parent_dir.
52
- data (bytes): the data want to write
53
- """
54
- fn_path = path
55
- if not os.path.isabs(fn_path) and len(self._parent_dir) > 0:
56
- fn_path = os.path.join(self._parent_dir, path)
57
-
58
- if not os.path.exists(os.path.dirname(fn_path)) and os.path.dirname(fn_path) != "":
59
- os.makedirs(os.path.dirname(fn_path), exist_ok=True)
60
-
61
- with open(fn_path, 'wb') as f:
62
- f.write(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/data_reader_writer/imagefile.py DELETED
@@ -1,66 +0,0 @@
1
- import os
2
- from io import BytesIO
3
- from PIL import Image
4
- from typing import Any, Dict, List
5
-
6
- class ImageDataWriter:
7
- def __init__(self, parent_dir: str = "", format = "JPEG") -> None:
8
-
9
- self._parent_dir = parent_dir
10
- self.images = {}
11
- self.format = format
12
-
13
- def add_image(self, img_bytes, image_name: str) -> None:
14
- self.images[image_name] = img_bytes
15
-
16
- def save_image(self, image_name: str) -> None:
17
- if image_name not in self.images:
18
- raise ValueError(f"Image {image_name} not found in memory.")
19
-
20
- img_bytes = self.images[image_name]
21
- file_path = os.path.join(self._parent_dir, image_name)
22
-
23
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
24
-
25
- with open(file_path, "wb") as f:
26
- f.write(img_bytes)
27
-
28
- def save_all_images(self):
29
- os.makedirs(self._parent_dir, exist_ok=True)
30
- for image_name in self.images:
31
- self.save_image(image_name)
32
- self.clear_buffer()
33
-
34
- def clear_buffer(self):
35
- self.images.clear()
36
-
37
-
38
-
39
- def replace_image_path_in_json(
40
- contents: Any,
41
- mapping: Dict[str, str],
42
- sub_type: List[str],
43
- key_name: str = "image_path",
44
- ) -> Any:
45
- def _replace_value(filename: str) -> str:
46
- if filename in mapping:
47
- return mapping[filename]
48
- raise KeyError(f"Missing mapping for {filename}")
49
-
50
- def _walk(obj: Any) -> Any:
51
- if isinstance(obj, dict):
52
- return {
53
- k: _replace_value(v) if k == key_name and isinstance(v, str) else _walk(v)
54
- for k, v in obj.items()
55
- }
56
-
57
- if isinstance(obj, list):
58
- return [_walk(i) for i in obj]
59
-
60
- return obj
61
-
62
- for i, content in enumerate(contents):
63
- if isinstance(content, dict) and content.get("type") in sub_type:
64
- contents[i] = _walk(content)
65
-
66
- return contents
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/engine.py DELETED
@@ -1,161 +0,0 @@
1
- import json
2
- import os
3
- import time
4
- from loguru import logger
5
-
6
- from NaviOCR.tools.pdf_image_tools import convert_pdf_bytes_to_bytes
7
- from NaviOCR.data_reader_writer import FileBasedDataWriter, ImageDataWriter
8
- from NaviOCR.tools.draw_bbox import draw_layout_bbox
9
- from NaviOCR.src.vlm_middle_json_mkcontent import union_make
10
- from NaviOCR.src.vlm_analyze import doc_analyze
11
- from NaviOCR.src.vlm_analyze import aio_doc_analyze
12
-
13
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
14
-
15
- def prepare_env(output_dir, pdf_file_name):
16
- local_md_dir = str(os.path.join(output_dir, pdf_file_name))
17
- local_image_dir = os.path.join(str(local_md_dir), "images")
18
- os.makedirs(local_image_dir, exist_ok=True)
19
- os.makedirs(local_md_dir, exist_ok=True)
20
- return local_image_dir, local_md_dir
21
-
22
-
23
- def _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids):
24
- result = []
25
- for idx, pdf_bytes in enumerate(pdf_bytes_list):
26
- valid_single_page_ids = (
27
- valid_page_ids[idx]
28
- if valid_page_ids and idx < len(valid_page_ids)
29
- else None
30
- )
31
- new_pdf_bytes = convert_pdf_bytes_to_bytes(pdf_bytes, valid_single_page_ids)
32
- result.append(new_pdf_bytes)
33
- return result
34
-
35
-
36
- def _process_output(
37
- pdf_info,
38
- pdf_bytes,
39
- pdf_file_name,
40
- local_md_dir,
41
- local_image_dir,
42
- md_writer,
43
- middle_json,
44
- f_draw_layout_bbox=True,
45
- f_dump_md=True,
46
- f_dump_middle_json=True,
47
- ):
48
- if f_draw_layout_bbox:
49
- draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf")
50
- image_dir = str(os.path.basename(local_image_dir))
51
- if f_dump_md:
52
- md_content_str = union_make(pdf_info, image_dir)
53
- md_writer.write_string(
54
- f"{pdf_file_name}.md",
55
- md_content_str,
56
- )
57
- if f_dump_middle_json:
58
- md_writer.write_string(
59
- f"{pdf_file_name}_middle.json",
60
- json.dumps(middle_json, ensure_ascii=False, indent=4),
61
- )
62
- logger.info(f"local output dir is {local_md_dir}")
63
-
64
- async def _async_process_vlm(output_dir, pdf_file_names, pdf_bytes_list, **kwargs,):
65
- results = []
66
- for idx, pdf_bytes in enumerate(pdf_bytes_list):
67
- pdf_file_name = pdf_file_names[idx]
68
- local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name)
69
- image_writer = ImageDataWriter(local_image_dir)
70
- vlm_doc_analyze_time = time.time()
71
- middle_json = await aio_doc_analyze(
72
- pdf_bytes, image_writer=image_writer, **kwargs,
73
- )
74
- vlm_doc_analyze_time = round(time.time() - vlm_doc_analyze_time, 2)
75
- logger.debug(f"doc_analyze cost: {vlm_doc_analyze_time}")
76
-
77
- pdf_info = middle_json["pdf_info"]
78
- md_writer = FileBasedDataWriter(local_md_dir)
79
-
80
- process_output_time = time.time()
81
- _process_output(
82
- pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir,
83
- md_writer, middle_json
84
- )
85
- image_writer.save_all_images()
86
- process_output_time = round(time.time() - process_output_time, 2)
87
- logger.debug(f"process_output_time cost: {process_output_time}")
88
- results.append(middle_json)
89
- return results
90
-
91
-
92
- def _process_vlm(output_dir, pdf_file_names, pdf_bytes_list, **kwargs,):
93
- results = []
94
- for idx, pdf_bytes in enumerate(pdf_bytes_list):
95
- pdf_file_name = pdf_file_names[idx]
96
- local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name)
97
- image_writer = ImageDataWriter(local_image_dir)
98
-
99
- vlm_doc_analyze_time = time.time()
100
- middle_json = doc_analyze(
101
- pdf_bytes, image_writer=image_writer, **kwargs,
102
- )
103
- vlm_doc_analyze_time = round(time.time() - vlm_doc_analyze_time, 2)
104
- logger.debug(f"doc_analyze cost: {vlm_doc_analyze_time}")
105
-
106
- pdf_info = middle_json["pdf_info"]
107
- md_writer = FileBasedDataWriter(local_md_dir)
108
-
109
- process_output_time = time.time()
110
- _process_output(
111
- pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir,
112
- md_writer, middle_json
113
- )
114
- image_writer.save_all_images()
115
- process_output_time = round(time.time() - process_output_time, 2)
116
- logger.debug(f"process_output_time cost: {process_output_time}")
117
-
118
- results.append(middle_json)
119
- return results
120
-
121
-
122
- def inplace_change_page_ids(results, valid_page_ids):
123
- for result, valid_page_id in zip(results, valid_page_ids):
124
- if valid_page_id is None:
125
- continue
126
- for page in result.get("pages", []):
127
- page_id = page.get("page_id")
128
- if (
129
- isinstance(page_id, int)
130
- and 0 <= page_id < len(valid_page_id)
131
- ):
132
- page["page_id"] = valid_page_id[page_id]
133
- return results
134
-
135
-
136
- async def aio_do_parse(
137
- output_dir,
138
- pdf_file_names: list[str],
139
- pdf_bytes_list: list[bytes],
140
- valid_page_ids: list[list[int] | None],
141
- **kwargs,
142
- ):
143
- pdf_bytes_list = _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids)
144
- Results = await _async_process_vlm(
145
- output_dir, pdf_file_names, pdf_bytes_list, **kwargs,
146
- )
147
- return Results
148
-
149
-
150
- def do_parse(
151
- output_dir,
152
- pdf_file_names: list[str],
153
- pdf_bytes_list: list[bytes],
154
- valid_page_ids: list[list[int] | None],
155
- **kwargs,
156
- ):
157
- pdf_bytes_list = _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids)
158
- Results = _process_vlm(
159
- output_dir, pdf_file_names, pdf_bytes_list, **kwargs,
160
- )
161
- return Results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/src/__init__.py DELETED
@@ -1 +0,0 @@
1
-
 
 
NaviOCR/src/model_output_to_middle_json.py DELETED
@@ -1,37 +0,0 @@
1
- from tqdm import tqdm
2
- from NaviOCR.tools.pdf_image_tools import get_page_size
3
- from NaviOCR.src.vlm_magic_model import MagicModel
4
- from NaviOCR.tools.cut_image import cut_image_and_table
5
- from NaviOCR.tools.enum_class import ContentType
6
- from NaviOCR.tools.hash_utils import bytes_md5
7
- from NaviOCR.version import __version__
8
-
9
-
10
- def blocks_to_page_info(page_blocks,image_dict,page,image_writer,page_index) -> dict:
11
- scale = image_dict["scale"]
12
- page_pil_img = image_dict["img_pil"]
13
- page_img_md5 = bytes_md5(page_pil_img.tobytes())
14
- width, height = map(int, get_page_size(page))
15
- magic_model = MagicModel(page_blocks, width, height)
16
- all_spans = magic_model.get_all_spans()
17
- for span in all_spans:
18
- if span["type"] in [
19
- ContentType.IMAGE,
20
- ContentType.SEAL,
21
- ContentType.CHAR,
22
- ]:
23
- cut_image_and_table(span,page_pil_img,page_img_md5,page_index,image_writer,scale=scale)
24
- page_blocks = magic_model.get_page_blocks()
25
- page_info = {"para_blocks": page_blocks,"discarded_blocks": [],"page_size": [width, height],"page_idx": page_index}
26
- return page_info
27
-
28
- def result_to_middle_json(model_output_blocks_list, images_list, pdf_doc, image_writer):
29
- middle_json = {"pdf_info": [],"_backend": "vlm","_version_name": __version__,}
30
- for index, page_blocks in enumerate(tqdm(model_output_blocks_list)):
31
- page = pdf_doc[index]
32
- image_dict = images_list[index]
33
- page_info = blocks_to_page_info(page_blocks,image_dict,page,image_writer,index)
34
- middle_json['pdf_info'].append(page_info)
35
-
36
- pdf_doc.close()
37
- return middle_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/src/vlm_analyze.py DELETED
@@ -1,63 +0,0 @@
1
- import time
2
- from loguru import logger
3
- from ..data_reader_writer import DataWriter
4
- from ..tools.enum_class import ImageType
5
- from .model_output_to_middle_json import result_to_middle_json
6
- from NaviOCR.vlm_utils.NaviOCR_model import NaviOCRMODEL_SERVICE
7
- from NaviOCR.tools.pdf_image_tools import load_images_from_pdf
8
- import NaviOCR.config as CONFIG
9
-
10
- def _init_model():
11
- backend=CONFIG.BACKEND
12
- model_path = CONFIG.model_path
13
- predictor = NaviOCRMODEL_SERVICE.get_model(backend, model_path, None)
14
- return predictor
15
-
16
- async def aio_doc_analyze(
17
- pdf_bytes,
18
- predictor = None,
19
- image_writer = None,
20
- ):
21
- if predictor is None:
22
- predictor = _init_model()
23
- load_images_start = time.time()
24
- images_list, pdf_doc = load_images_from_pdf(pdf_bytes, image_type=ImageType.PIL, threads=CONFIG.PDF_TOOLS_WORKER_MAX_NUM)
25
- images_pil_list = [image_dict["img_pil"] for image_dict in images_list]
26
- load_images_time = round(time.time() - load_images_start, 2)
27
- logger.debug(f"load images cost: {load_images_time}, speed: {round(load_images_time/len(images_pil_list), 3)} images/s")
28
- infer_start = time.time()
29
- results = await predictor.aio_batch_two_step_extract(images=images_pil_list)
30
- infer_time = round(time.time() - infer_start, 2)
31
- logger.debug(f"infer finished, cost: {infer_time}, speed: {round(infer_time / len(results), 3)} page/s")
32
-
33
- output_start = time.time()
34
- middle_json = result_to_middle_json(results, images_list, pdf_doc, image_writer)
35
- output_time = round(time.time() - output_start, 2)
36
- logger.debug(f"output json finished, cost: {output_time}, speed: {round(output_time / len(results), 3)} page/s")
37
- return middle_json
38
-
39
-
40
- def doc_analyze(
41
- pdf_bytes,
42
- image_writer: DataWriter | None,
43
- predictor = None,
44
- ):
45
- if predictor is None:
46
- predictor = _init_model()
47
-
48
- load_images_start = time.time()
49
- images_list, pdf_doc = load_images_from_pdf(pdf_bytes, image_type=ImageType.PIL, threads=CONFIG.PDF_TOOLS_WORKER_MAX_NUM)
50
- images_pil_list = [image_dict["img_pil"] for image_dict in images_list]
51
- load_images_time = round(time.time() - load_images_start, 2)
52
- logger.debug(f"load images cost: {load_images_time}, speed: {round(load_images_time/len(images_pil_list), 3)} images/s")
53
-
54
- infer_start = time.time()
55
- results = predictor.batch_two_step_extract(images=images_pil_list)
56
- infer_time = round(time.time() - infer_start, 2)
57
- logger.debug(f"infer finished, cost: {infer_time}, speed: {round(len(results)/infer_time, 3)} page/s")
58
-
59
- output_start = time.time()
60
- middle_json = result_to_middle_json(results, images_list, pdf_doc, image_writer)
61
- output_time = round(time.time() - output_start, 2)
62
- logger.debug(f"output json finished, cost: {output_time}, speed: {round(output_time / len(results), 3)} page/s")
63
- return middle_json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/src/vlm_magic_model.py DELETED
@@ -1,561 +0,0 @@
1
- import re
2
- from typing import Literal
3
-
4
- from loguru import logger
5
-
6
- from NaviOCR.tools.boxbase import calculate_overlap_area_in_bbox1_area_ratio
7
- from NaviOCR.tools.enum_class import ContentType, BlockType
8
- from NaviOCR.tools.guess_suffix_or_lang import guess_language_by_text
9
- from NaviOCR.tools.magic_model_utils import reduct_overlap, tie_up_category_by_index
10
-
11
-
12
- class MagicModel:
13
- def __init__(self, page_blocks: list, width, height):
14
- self.page_blocks = page_blocks
15
-
16
- blocks = []
17
- self.all_spans = []
18
- for index, block_info in enumerate(page_blocks):
19
- block_bbox = block_info["bbox"]
20
- try:
21
- if len(block_bbox) % 2 != 0:
22
- logger.warning(f"Invalid bbox length: {block_bbox}")
23
- else:
24
- pts = []
25
- for i in range(0, len(block_bbox), 2):
26
- pts.append(int(block_bbox[i] * width))
27
- pts.append(int(block_bbox[i + 1] * height))
28
- block_bbox = tuple(pts)
29
- block_type = block_info["type"]
30
- block_content = block_info["content"]
31
- block_angle = block_info["angle"]
32
- except Exception as e:
33
- logger.warning(f"Invalid block format: {block_info}, error: {e}")
34
- continue
35
-
36
- span_type = "unknown"
37
- code_block_sub_type = None
38
- guess_lang = None
39
-
40
- # 确定类型
41
- if block_type in ["list"]:
42
- span_type = ContentType.TEXT
43
-
44
- elif block_type in [
45
- "text",
46
- "title",
47
- "image_caption",
48
- "image_footnote",
49
- "table_caption",
50
- "table_footnote",
51
- "code_caption",
52
- "ref_text",
53
- "phonetic",
54
- "header",
55
- "footer",
56
- "page_number",
57
- "aside_text",
58
- "page_footnote",
59
- ]:
60
- span_type = ContentType.TEXT
61
- elif block_type in ["image"]:
62
- block_type = BlockType.IMAGE_BODY
63
- span_type = ContentType.IMAGE
64
- elif block_type in ["table"]:
65
- block_type = BlockType.TABLE_BODY
66
- span_type = ContentType.TABLE
67
- elif block_type in ["code", "algorithm"]:
68
- guess_lang, block_content = guess_language_by_text(block_content)
69
- code_block_sub_type = block_type
70
- block_type = BlockType.CODE_BODY
71
- span_type = ContentType.TEXT
72
- elif block_type in ["equation"]:
73
- block_type = BlockType.INTERLINE_EQUATION
74
- span_type = ContentType.INTERLINE_EQUATION
75
- elif block_type in ['seal']:
76
- block_type = BlockType.SEAL
77
- span_type = ContentType.SEAL
78
- elif block_type in ['char']:
79
- block_type = BlockType.CHAR
80
- span_type = ContentType.CHAR
81
-
82
- switch_code_to_algorithm = False
83
-
84
- if span_type in [ContentType.IMAGE, ContentType.TABLE]:
85
- span = {
86
- "bbox": block_bbox,
87
- "type": span_type,
88
- }
89
- if span_type == ContentType.TABLE:
90
- span["html"] = block_content
91
-
92
- elif span_type in [ContentType.INTERLINE_EQUATION]:
93
- span = {
94
- "bbox": block_bbox,
95
- "type": span_type,
96
- "content": block_content,
97
- }
98
- elif span_type in [ContentType.SEAL]:
99
- span = {
100
- "bbox": block_bbox,
101
- "type": span_type,
102
- "content": block_content
103
- }
104
- elif span_type in [ContentType.CHAR]:
105
- span = {
106
- "bbox": block_bbox,
107
- "type": span_type,
108
- "content": block_content
109
- }
110
- else:
111
- if block_type == "title" and block_content:
112
- block_content = re.sub(r"\n\s*", " ", block_content).strip()
113
- span = {
114
- "bbox": block_bbox,
115
- "type": span_type,
116
- "content": block_content,
117
- }
118
-
119
- if isinstance(span, dict) and "bbox" in span:
120
- self.all_spans.append(span)
121
- spans = [span]
122
- elif isinstance(span, list):
123
- self.all_spans.extend(span)
124
- spans = span
125
- else:
126
- raise ValueError(f"Invalid span type: {span_type}, expected dict or list, got {type(span)}")
127
-
128
- if block_type in [BlockType.CODE_BODY]:
129
- if switch_code_to_algorithm and code_block_sub_type == "code":
130
- code_block_sub_type = "algorithm"
131
- line = {"bbox": block_bbox, "spans": spans, "extra": {"type": code_block_sub_type, "guess_lang": guess_lang}}
132
- else:
133
- line = {"bbox": block_bbox, "spans": spans}
134
-
135
- blocks.append(
136
- {
137
- "bbox": block_bbox,
138
- "type": block_type,
139
- "angle": block_angle,
140
- "lines": [line],
141
- "index": index,
142
- }
143
- )
144
-
145
- # 收集各式的blocks
146
- self.image_blocks = []
147
- self.table_blocks = []
148
- self.interline_equation_blocks = []
149
- self.text_blocks = []
150
- self.title_blocks = []
151
- self.code_blocks = []
152
- self.discarded_blocks = []
153
- self.ref_text_blocks = []
154
- self.phonetic_blocks = []
155
- self.list_blocks = []
156
- self.seal_blocks = []
157
- self.char_blocks = []
158
- for block in blocks:
159
- if block["type"] in [BlockType.IMAGE_BODY, BlockType.IMAGE_CAPTION, BlockType.IMAGE_FOOTNOTE]:
160
- self.image_blocks.append(block)
161
- elif block["type"] in [BlockType.TABLE_BODY, BlockType.TABLE_CAPTION, BlockType.TABLE_FOOTNOTE]:
162
- self.table_blocks.append(block)
163
- elif block["type"] in [BlockType.CODE_BODY, BlockType.CODE_CAPTION]:
164
- self.code_blocks.append(block)
165
- elif block["type"] == BlockType.INTERLINE_EQUATION:
166
- self.interline_equation_blocks.append(block)
167
- elif block["type"] == BlockType.TEXT:
168
- self.text_blocks.append(block)
169
- elif block["type"] == BlockType.TITLE:
170
- self.title_blocks.append(block)
171
- elif block["type"] in [BlockType.REF_TEXT]:
172
- self.ref_text_blocks.append(block)
173
- elif block["type"] in [BlockType.PHONETIC]:
174
- self.phonetic_blocks.append(block)
175
- elif block["type"] in [BlockType.HEADER, BlockType.FOOTER, BlockType.PAGE_NUMBER, BlockType.ASIDE_TEXT, BlockType.PAGE_FOOTNOTE]:
176
- self.discarded_blocks.append(block)
177
- elif block["type"] == BlockType.LIST:
178
- self.list_blocks.append(block)
179
- elif block["type"] == BlockType.SEAL:
180
- self.seal_blocks.append(block)
181
- elif block["type"] == BlockType.CHAR:
182
- self.char_blocks.append(block)
183
- else:
184
- continue
185
-
186
-
187
- self.list_blocks, self.text_blocks, self.ref_text_blocks = fix_list_blocks(self.list_blocks, self.text_blocks, self.ref_text_blocks)
188
- self.image_blocks, not_include_image_blocks = fix_two_layer_blocks(self.image_blocks, BlockType.IMAGE)
189
- self.table_blocks, not_include_table_blocks = fix_two_layer_blocks(self.table_blocks, BlockType.TABLE)
190
- self.code_blocks, not_include_code_blocks = fix_two_layer_blocks(self.code_blocks, BlockType.CODE)
191
- for code_block in self.code_blocks:
192
- for block in code_block['blocks']:
193
- if block['type'] == BlockType.CODE_BODY:
194
- if len(block["lines"]) > 0:
195
- line = block["lines"][0]
196
- code_block["sub_type"] = line["extra"]["type"]
197
- if code_block["sub_type"] in ["code"]:
198
- code_block["guess_lang"] = line["extra"]["guess_lang"]
199
- del line["extra"]
200
- else:
201
- code_block["sub_type"] = "code"
202
- code_block["guess_lang"] = "txt"
203
-
204
-
205
- for block in not_include_image_blocks + not_include_table_blocks + not_include_code_blocks:
206
- block["type"] = BlockType.TEXT
207
- self.text_blocks.append(block)
208
-
209
-
210
- def get_list_blocks(self):
211
-
212
- return self.list_blocks
213
-
214
- def get_image_blocks(self):
215
-
216
- return self.image_blocks
217
-
218
- def get_table_blocks(self):
219
-
220
- return self.table_blocks
221
-
222
- def get_code_blocks(self):
223
-
224
- return self.code_blocks
225
-
226
- def get_ref_text_blocks(self):
227
-
228
- return self.ref_text_blocks
229
-
230
- def get_phonetic_blocks(self):
231
-
232
- return self.phonetic_blocks
233
-
234
- def get_title_blocks(self):
235
-
236
- return self.title_blocks
237
-
238
- def get_text_blocks(self):
239
-
240
- return self.text_blocks
241
-
242
- def get_interline_equation_blocks(self):
243
-
244
- return self.interline_equation_blocks
245
-
246
- def get_discarded_blocks(self):
247
-
248
- return self.discarded_blocks
249
-
250
- def get_all_spans(self):
251
- return self.all_spans
252
-
253
- def get_content_blocks(self, content_blocks):
254
- new_content_blocks = []
255
- for inter_block in content_blocks:
256
- contents = extract_contents(inter_block.get('lines', []), 'content')
257
- if len(contents) > 0:
258
- new_content_blocks.append({
259
- "type":inter_block.get("type", "text"),
260
- "angle":inter_block.get("angle", 0),
261
- "index":inter_block.get('index', -1),
262
- "pos":inter_block.get('bbox', []),
263
- "text":contents[0],
264
- })
265
- return new_content_blocks
266
-
267
- def get_seal_blocks(self):
268
-
269
- return self.seal_blocks
270
-
271
-
272
- def get_char_blocks(self):
273
-
274
- return self.char_blocks
275
-
276
- def get_page_blocks(self, pass_blocks=False):
277
- image_blocks = self.get_image_blocks()
278
- table_blocks = self.get_table_blocks()
279
- title_blocks = self.get_title_blocks()
280
- discarded_blocks = self.get_discarded_blocks()
281
- code_blocks = self.get_code_blocks()
282
- ref_text_blocks = self.get_ref_text_blocks()
283
- phonetic_blocks = self.get_phonetic_blocks()
284
- list_blocks = self.get_list_blocks()
285
- text_blocks = self.get_text_blocks()
286
- interline_equation_blocks = self.get_interline_equation_blocks()
287
- seal_blocks = self.get_seal_blocks()
288
- char_blocks = self.get_char_blocks()
289
-
290
- if pass_blocks:
291
- page_blocks = [
292
- *table_blocks,
293
- *code_blocks,
294
- *ref_text_blocks,
295
- *phonetic_blocks,
296
- *title_blocks,
297
- *text_blocks,
298
- *interline_equation_blocks,
299
- *discarded_blocks,
300
- ]
301
- else:
302
- page_blocks = [
303
- *image_blocks,
304
- *table_blocks,
305
- *code_blocks,
306
- *ref_text_blocks,
307
- *phonetic_blocks,
308
- *title_blocks,
309
- *text_blocks,
310
- *interline_equation_blocks,
311
- *list_blocks,
312
- *discarded_blocks,
313
- *seal_blocks,
314
- *char_blocks,
315
- ]
316
- page_blocks.sort(key=lambda x: x["index"])
317
- for i, block in enumerate(page_blocks):
318
- block["index"] = i
319
- return page_blocks
320
-
321
-
322
-
323
-
324
- def extract_contents(data, key):
325
- image_paths = []
326
- def _walk(obj):
327
- if isinstance(obj, dict):
328
- if key in obj:
329
- image_paths.append(obj[key])
330
- for v in obj.values():
331
- _walk(v)
332
- elif isinstance(obj, list):
333
- for item in obj:
334
- _walk(item)
335
- _walk(data)
336
- return image_paths
337
-
338
-
339
- def __tie_up_category_by_index(blocks, subject_block_type, object_block_type):
340
- """基于index的主客体关联包装函数"""
341
- # 定义获取主体和客体对象的函数
342
- def get_subjects():
343
- return reduct_overlap(
344
- list(
345
- map(
346
- lambda x: {"bbox": x["bbox"], "lines": x["lines"], "index": x["index"], "angle": x["angle"]},
347
- filter(
348
- lambda x: x["type"] == subject_block_type,
349
- blocks,
350
- ),
351
- )
352
- )
353
- )
354
-
355
- def get_objects():
356
- return reduct_overlap(
357
- list(
358
- map(
359
- lambda x: {"bbox": x["bbox"], "lines": x["lines"], "index": x["index"], "angle": x["angle"]},
360
- filter(
361
- lambda x: x["type"] == object_block_type,
362
- blocks,
363
- ),
364
- )
365
- )
366
- )
367
-
368
- # 调用通用方法
369
- return tie_up_category_by_index(
370
- get_subjects,
371
- get_objects
372
- )
373
-
374
-
375
- def get_type_blocks(blocks, block_type: Literal["image", "table", "code"]):
376
- with_captions = __tie_up_category_by_index(blocks, f"{block_type}_body", f"{block_type}_caption")
377
- with_footnotes = __tie_up_category_by_index(blocks, f"{block_type}_body", f"{block_type}_footnote")
378
- ret = []
379
- for v in with_captions:
380
- record = {
381
- f"{block_type}_body": v["sub_bbox"],
382
- f"{block_type}_caption_list": v["obj_bboxes"],
383
- }
384
- filter_idx = v["sub_idx"]
385
- d = next(filter(lambda x: x["sub_idx"] == filter_idx, with_footnotes))
386
- record[f"{block_type}_footnote_list"] = d["obj_bboxes"]
387
- ret.append(record)
388
- return ret
389
-
390
-
391
- def fix_two_layer_blocks(blocks, fix_type: Literal["image", "table", "code"]):
392
- need_fix_blocks = get_type_blocks(blocks, fix_type)
393
- fixed_blocks = []
394
- not_include_blocks = []
395
- processed_indices = set()
396
-
397
- # 特殊处理表格类型,确保标题在表格前,注脚在表格后
398
- if fix_type in ["table", "image"]:
399
- # 收集所有不合适的caption和footnote
400
- misplaced_captions = [] # 存储(caption, 原始block索引)
401
- misplaced_footnotes = [] # 存储(footnote, 原始block索引)
402
-
403
- # 第一步:移除不符合位置要求的footnote
404
- for block_idx, block in enumerate(need_fix_blocks):
405
- body = block[f"{fix_type}_body"]
406
- body_index = body["index"]
407
-
408
- # 检查footnote应在body后或同位置
409
- valid_footnotes = []
410
- for footnote in block[f"{fix_type}_footnote_list"]:
411
- if footnote["index"] >= body_index:
412
- valid_footnotes.append(footnote)
413
- else:
414
- misplaced_footnotes.append((footnote, block_idx))
415
- block[f"{fix_type}_footnote_list"] = valid_footnotes
416
-
417
- # 第三步:重新分配不合规的footnote到合适的body
418
- for footnote, original_block_idx in misplaced_footnotes:
419
- footnote_index = footnote["index"]
420
- best_block_idx = None
421
- min_distance = float('inf')
422
-
423
- # 寻找索引小于等于footnote_index的最近body
424
- for idx, block in enumerate(need_fix_blocks):
425
- body_index = block[f"{fix_type}_body"]["index"]
426
- if body_index <= footnote_index and idx != original_block_idx:
427
- distance = footnote_index - body_index
428
- if distance < min_distance:
429
- min_distance = distance
430
- best_block_idx = idx
431
-
432
- if best_block_idx is not None:
433
- # 找到合适的body,添加到对应block的footnote_list
434
- need_fix_blocks[best_block_idx][f"{fix_type}_footnote_list"].append(footnote)
435
- else:
436
- # 没找到合适的body,作为普通block处理
437
- not_include_blocks.append(footnote)
438
-
439
- # 第四步:将每个block的caption_list和footnote_list中不连续index的元素提出来作为普通block处理
440
- for block in need_fix_blocks:
441
- caption_list = block[f"{fix_type}_caption_list"]
442
- footnote_list = block[f"{fix_type}_footnote_list"]
443
- body_index = block[f"{fix_type}_body"]["index"]
444
-
445
- # 处理caption_list (从body往前看,caption在body之前)
446
- if caption_list:
447
- # 按index降序排列,从最接近body的开始检查
448
- caption_list.sort(key=lambda x: x["index"], reverse=True)
449
- filtered_captions = [caption_list[0]]
450
- for i in range(1, len(caption_list)):
451
- prev_index = caption_list[i - 1]["index"]
452
- curr_index = caption_list[i]["index"]
453
-
454
- # 检查是否连续
455
- if curr_index == prev_index - 1:
456
- filtered_captions.append(caption_list[i])
457
- else:
458
- # 检查gap中是否只有body_index
459
- gap_indices = set(range(curr_index + 1, prev_index))
460
- if gap_indices == {body_index}:
461
- # gap中只有body_index,不算真正的gap
462
- filtered_captions.append(caption_list[i])
463
- else:
464
- # 出现真正的gap,后续所有caption都作为普通block
465
- not_include_blocks.extend(caption_list[i:])
466
- break
467
- # 恢复升序
468
- filtered_captions.reverse()
469
- block[f"{fix_type}_caption_list"] = filtered_captions
470
-
471
- # 处理footnote_list (从body往后看,footnote在body之后)
472
- if footnote_list:
473
- # 按index升序排列,从最接近body的开始检查
474
- footnote_list.sort(key=lambda x: x["index"])
475
- filtered_footnotes = [footnote_list[0]]
476
- for i in range(1, len(footnote_list)):
477
- # 检查是否与前一个footnote连续
478
- if footnote_list[i]["index"] == footnote_list[i - 1]["index"] + 1:
479
- filtered_footnotes.append(footnote_list[i])
480
- else:
481
- # 出现gap,后续所有footnote都作为普通block
482
- not_include_blocks.extend(footnote_list[i:])
483
- break
484
- block[f"{fix_type}_footnote_list"] = filtered_footnotes
485
-
486
- # 构建两层结构blocks
487
- for block in need_fix_blocks:
488
- body = block[f"{fix_type}_body"]
489
- caption_list = block[f"{fix_type}_caption_list"]
490
- footnote_list = block[f"{fix_type}_footnote_list"]
491
-
492
- body["type"] = f"{fix_type}_body"
493
- for caption in caption_list:
494
- caption["type"] = f"{fix_type}_caption"
495
- processed_indices.add(caption["index"])
496
- for footnote in footnote_list:
497
- footnote["type"] = f"{fix_type}_footnote"
498
- processed_indices.add(footnote["index"])
499
-
500
- processed_indices.add(body["index"])
501
-
502
- two_layer_block = {
503
- "type": fix_type,
504
- "bbox": body["bbox"],
505
- "blocks": [body],
506
- "index": body["index"],
507
- }
508
- two_layer_block["blocks"].extend([*caption_list, *footnote_list])
509
- # 对blocks按index排序
510
- two_layer_block["blocks"].sort(key=lambda x: x["index"])
511
-
512
- fixed_blocks.append(two_layer_block)
513
-
514
- # 添加未处理的blocks
515
- for block in blocks:
516
- block.pop("type", None)
517
- if block["index"] not in processed_indices and block not in not_include_blocks:
518
- not_include_blocks.append(block)
519
-
520
- return fixed_blocks, not_include_blocks
521
-
522
-
523
- def fix_list_blocks(list_blocks, text_blocks, ref_text_blocks):
524
- for list_block in list_blocks:
525
- list_block["blocks"] = []
526
- if "lines" in list_block:
527
- del list_block["lines"]
528
-
529
- temp_text_blocks = text_blocks + ref_text_blocks
530
- need_remove_blocks = []
531
- for block in temp_text_blocks:
532
- for list_block in list_blocks:
533
- if calculate_overlap_area_in_bbox1_area_ratio(block["bbox"], list_block["bbox"]) >= 0.8:
534
- list_block["blocks"].append(block)
535
- need_remove_blocks.append(block)
536
- break
537
-
538
-
539
- for block in need_remove_blocks:
540
- if block in text_blocks:
541
- text_blocks.remove(block)
542
- elif block in ref_text_blocks:
543
- ref_text_blocks.remove(block)
544
-
545
- # 移除blocks为空的list_block
546
- list_blocks = [lb for lb in list_blocks if lb["blocks"]]
547
-
548
- for list_block in list_blocks:
549
- # 统计list_block["blocks"]中所有block的type,用众数作为list_block的sub_type
550
- type_count = {}
551
- for sub_block in list_block["blocks"]:
552
- sub_block_type = sub_block["type"]
553
- if sub_block_type not in type_count:
554
- type_count[sub_block_type] = 0
555
- type_count[sub_block_type] += 1
556
-
557
- if type_count:
558
- list_block["sub_type"] = max(type_count, key=type_count.get)
559
- else:
560
- list_block["sub_type"] = "unknown"
561
- return list_blocks, text_blocks, ref_text_blocks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/src/vlm_middle_json_mkcontent.py DELETED
@@ -1,176 +0,0 @@
1
- import os
2
-
3
- from loguru import logger
4
-
5
- from NaviOCR.tools.char_utils import full_to_half_exclude_marks, is_hyphen_at_line_end
6
- from NaviOCR.tools.config_reader import get_latex_delimiter_config
7
- from NaviOCR.tools.enum_class import BlockType, ContentType
8
- from NaviOCR.tools.language import detect_lang
9
-
10
-
11
- def merge_para_with_text(para_block):
12
- block_text = ''
13
- for line in para_block['lines']:
14
- for span in line['spans']:
15
- if span['type'] in [ContentType.TEXT]:
16
- try:
17
- span['content'] = full_to_half_exclude_marks(span['content'])
18
- except:
19
- continue
20
- block_text += span['content']
21
- block_lang = detect_lang(block_text)
22
-
23
- para_text = ''
24
- for i, line in enumerate(para_block['lines']):
25
- for j, span in enumerate(line['spans']):
26
- span_type = span['type']
27
- content = ''
28
- if span_type == ContentType.TEXT:
29
- content = span['content']
30
- elif span_type == ContentType.INLINE_EQUATION:
31
- content = span['content']
32
- elif span_type == ContentType.INTERLINE_EQUATION:
33
- content = span['content']
34
- try:
35
- content = content.strip()
36
- except:
37
- continue
38
- if content:
39
- if span_type == ContentType.INTERLINE_EQUATION:
40
- para_text += content
41
- continue
42
- cjk_langs = {'zh', 'ja', 'ko'}
43
- is_last_span = j == len(line['spans']) - 1
44
-
45
- if block_lang in cjk_langs:
46
- if is_last_span and span_type != ContentType.INLINE_EQUATION:
47
- para_text += content
48
- else:
49
- para_text += f'{content} '
50
- else:
51
- if span_type in [ContentType.TEXT, ContentType.INLINE_EQUATION]:
52
- if (
53
- is_last_span
54
- and span_type == ContentType.TEXT
55
- and is_hyphen_at_line_end(content)
56
- ):
57
- if (
58
- i+1 < len(para_block['lines'])
59
- and para_block['lines'][i + 1].get('spans')
60
- and para_block['lines'][i + 1]['spans'][0].get('type') == ContentType.TEXT
61
- and para_block['lines'][i + 1]['spans'][0].get('content', '')
62
- and para_block['lines'][i + 1]['spans'][0]['content'][0].islower()
63
- ):
64
- para_text += content[:-1]
65
- else:
66
- para_text += content
67
- else:
68
- para_text += f'{content} '
69
- return para_text
70
-
71
-
72
- def mk_blocks_to_markdown(para_blocks, img_buket_path=''):
73
- page_markdown = []
74
- for para_block in para_blocks:
75
- para_text = ''
76
- para_type = para_block['type']
77
- if para_type in [BlockType.TEXT, BlockType.INTERLINE_EQUATION, BlockType.PHONETIC, BlockType.REF_TEXT, BlockType.FOOTER, BlockType.HEADER]:
78
- para_text = merge_para_with_text(para_block)
79
- elif para_type == BlockType.LIST:
80
- for block in para_block['blocks']:
81
- item_text = merge_para_with_text(block)
82
- para_text += f"{item_text} \n"
83
- elif para_type == BlockType.TITLE:
84
- title_level = get_title_level(para_block)
85
- para_text = f'{"#" * title_level} {merge_para_with_text(para_block)}'
86
- elif para_type == BlockType.IMAGE:
87
- has_image_footnote = any(block['type'] == BlockType.IMAGE_FOOTNOTE for block in para_block['blocks'])
88
- if has_image_footnote:
89
- for block in para_block['blocks']: # 1st.拼image_caption
90
- if block['type'] == BlockType.IMAGE_CAPTION:
91
- para_text += merge_para_with_text(block) + ' \n'
92
- for block in para_block['blocks']: # 2nd.拼image_body
93
- if block['type'] == BlockType.IMAGE_BODY:
94
- for line in block['lines']:
95
- for span in line['spans']:
96
- if span['type'] == ContentType.IMAGE:
97
- if span.get('image_path', ''):
98
- para_text += f"![]({img_buket_path}/{span['image_path']})"
99
- for block in para_block['blocks']: # 3rd.拼image_footnote
100
- if block['type'] == BlockType.IMAGE_FOOTNOTE:
101
- para_text += ' \n' + merge_para_with_text(block)
102
- else:
103
- for block in para_block['blocks']:
104
- if block['type'] == BlockType.IMAGE_BODY:
105
- for line in block['lines']:
106
- for span in line['spans']:
107
- if span['type'] == ContentType.IMAGE:
108
- if span.get('image_path', ''):
109
- para_text += f"![]({img_buket_path}/{span['image_path']})"
110
- for block in para_block['blocks']:
111
- if block['type'] == BlockType.IMAGE_CAPTION:
112
- para_text += ' \n' + merge_para_with_text(block)
113
- elif para_type == BlockType.TABLE:
114
- for block in para_block['blocks']:
115
- if block['type'] == BlockType.TABLE_CAPTION:
116
- para_text += merge_para_with_text(block) + ' \n'
117
- for block in para_block['blocks']:
118
- if block['type'] == BlockType.TABLE_BODY:
119
- for line in block['lines']:
120
- for span in line['spans']:
121
- if span['type'] == ContentType.TABLE:
122
- if span.get('html', ''):
123
- para_text += f"\n{span['html']}\n"
124
- elif span.get('image_path', ''):
125
- para_text += f"![]({img_buket_path}/{span['image_path']})"
126
- for block in para_block['blocks']:
127
- if block['type'] == BlockType.TABLE_FOOTNOTE:
128
- para_text += '\n' + merge_para_with_text(block) + ' '
129
- elif para_type == BlockType.CODE:
130
- sub_type = para_block["sub_type"]
131
- for block in para_block['blocks']:
132
- if block['type'] == BlockType.CODE_CAPTION:
133
- para_text += merge_para_with_text(block) + ' \n'
134
- for block in para_block['blocks']:
135
- if block['type'] == BlockType.CODE_BODY:
136
- if sub_type == BlockType.CODE:
137
- guess_lang = para_block["guess_lang"]
138
- para_text += f"```{guess_lang}\n{merge_para_with_text(block)}\n```"
139
- elif sub_type == BlockType.ALGORITHM:
140
- para_text += merge_para_with_text(block)
141
- elif para_type == BlockType.CHAR:
142
- for line in para_block['lines']:
143
- spans = line['spans']
144
- for span in spans:
145
- content = span['content']
146
- if len(content) <= 5:
147
- continue
148
- else:
149
- para_text += f"\n{content}\n"
150
- if para_text.strip() == '':
151
- continue
152
- else:
153
- page_markdown.append(para_text.strip())
154
-
155
- return page_markdown
156
-
157
-
158
- def union_make(pdf_info_dict: list,img_buket_path: str = '', ):
159
- output_content = []
160
- for page_info in pdf_info_dict:
161
- paras_of_layout = page_info.get('para_blocks')
162
-
163
- if not paras_of_layout:
164
- continue
165
- page_markdown = mk_blocks_to_markdown(paras_of_layout, img_buket_path)
166
- output_content.extend(page_markdown)
167
-
168
- return '\n\n'.join(output_content)
169
-
170
- def get_title_level(block):
171
- title_level = block.get('level', 1)
172
- if title_level > 4:
173
- title_level = 4
174
- elif title_level < 1:
175
- title_level = 0
176
- return title_level
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/boxbase.py DELETED
@@ -1,224 +0,0 @@
1
- import math
2
-
3
- def normalize_bbox(bbox):
4
- if isinstance(bbox[0], (int, float)) and len(bbox) == 4:
5
- x0, y0, x1, y1 = bbox
6
- return x0, y0, x1, y1
7
- if isinstance(bbox[0], (int, float)) and len(bbox) % 2 == 0:
8
- xs = bbox[0::2]
9
- ys = bbox[1::2]
10
- return min(xs), min(ys), max(xs), max(ys)
11
- xs = [p[0] for p in bbox]
12
- ys = [p[1] for p in bbox]
13
- return min(xs), min(ys), max(xs), max(ys)
14
-
15
- def is_in(box1, box2) -> bool:
16
- x0_1, y0_1, x1_1, y1_1 = normalize_bbox(box1)
17
- x0_2, y0_2, x1_2, y1_2 = normalize_bbox(box2)
18
- return (
19
- x0_1 >= x0_2
20
- and y0_1 >= y0_2
21
- and x1_1 <= x1_2
22
- and y1_1 <= y1_2
23
- )
24
-
25
- def bbox_relative_pos(bbox1, bbox2):
26
- """判断两个矩形框的相对位置关系.
27
-
28
- Args:
29
- bbox1: 一个四元组,表示第一个矩形框的左上角和右下角的坐标,格式为(x1, y1, x1b, y1b)
30
- bbox2: 一个四元组,表示第二个矩形框的左上角和右下角的坐标,格式为(x2, y2, x2b, y2b)
31
-
32
- Returns:
33
- 一个四元组,表示矩形框1相对于矩形框2的位置关系,格式为(left, right, bottom, top)
34
- 其中,left表示矩形框1是否在矩形框2的左侧,right表示矩形框1是否在矩形框2的右侧,
35
- bottom表示矩形框1是否在矩形框2的下方,top表示矩形框1是否在矩形框2的上方
36
- """
37
- x1, y1, x1b, y1b = bbox1
38
- x2, y2, x2b, y2b = bbox2
39
-
40
- left = x2b < x1
41
- right = x1b < x2
42
- bottom = y2b < y1
43
- top = y1b < y2
44
- return left, right, bottom, top
45
-
46
-
47
- def bbox_distance(bbox1, bbox2):
48
- """计算两个矩形框的距离。
49
- Args:
50
- bbox1 (tuple): 第一个矩形框的坐标,格式为 (x1, y1, x2, y2),其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
51
- bbox2 (tuple): 第二个矩形框的坐标,格式为 (x1, y1, x2, y2),其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
52
- Returns:
53
- float: 矩形框之间的距离。
54
- """
55
-
56
- def dist(point1, point2):
57
- return math.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)
58
-
59
- x1, y1, x1b, y1b = bbox1
60
- x2, y2, x2b, y2b = bbox2
61
-
62
- left, right, bottom, top = bbox_relative_pos(bbox1, bbox2)
63
-
64
- if top and left:
65
- return dist((x1, y1b), (x2b, y2))
66
- elif left and bottom:
67
- return dist((x1, y1), (x2b, y2b))
68
- elif bottom and right:
69
- return dist((x1b, y1), (x2, y2b))
70
- elif right and top:
71
- return dist((x1b, y1b), (x2, y2))
72
- elif left:
73
- return x1 - x2b
74
- elif right:
75
- return x2 - x1b
76
- elif bottom:
77
- return y1 - y2b
78
- elif top:
79
- return y2 - y1b
80
- return 0.0
81
-
82
-
83
- def _bbox_center(bbox):
84
- if len(bbox) < 4 or len(bbox) % 2 != 0:
85
- raise ValueError("bbox must contain an even number of coordinates (>=4)")
86
- xs = bbox[0::2]
87
- ys = bbox[1::2]
88
- n = len(xs)
89
- return sum(xs) / n, sum(ys) / n
90
-
91
-
92
- def bbox_center_distance(bbox1, bbox2):
93
- c1x, c1y = _bbox_center(bbox1)
94
- c2x, c2y = _bbox_center(bbox2)
95
- return math.sqrt((c1x - c2x) ** 2 + (c1y - c2y) ** 2)
96
-
97
-
98
- def get_minbox_if_overlap_by_ratio(bbox1, bbox2, ratio):
99
- """通过calculate_overlap_area_2_minbox_area_ratio计算两个bbox重叠的面积占最小面积的box的比例
100
- 如果比例大于ratio,则返回小的那个bbox, 否则返回None."""
101
- x1_min, y1_min, x1_max, y1_max = bbox1
102
- x2_min, y2_min, x2_max, y2_max = bbox2
103
- area1 = (x1_max - x1_min) * (y1_max - y1_min)
104
- area2 = (x2_max - x2_min) * (y2_max - y2_min)
105
- overlap_ratio = calculate_overlap_area_2_minbox_area_ratio(bbox1, bbox2)
106
- if overlap_ratio > ratio:
107
- if area1 <= area2:
108
- return bbox1
109
- else:
110
- return bbox2
111
- else:
112
- return None
113
-
114
-
115
- def calculate_overlap_area_2_minbox_area_ratio(bbox1, bbox2):
116
- """计算box1和box2的重叠面积占最小面积的box的比例."""
117
- # Determine the coordinates of the intersection rectangle
118
- x_left = max(bbox1[0], bbox2[0])
119
- y_top = max(bbox1[1], bbox2[1])
120
- x_right = min(bbox1[2], bbox2[2])
121
- y_bottom = min(bbox1[3], bbox2[3])
122
-
123
- if x_right < x_left or y_bottom < y_top:
124
- return 0.0
125
-
126
- # The area of overlap area
127
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
128
- min_box_area = min([(bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]),
129
- (bbox2[3] - bbox2[1]) * (bbox2[2] - bbox2[0])])
130
- if min_box_area == 0:
131
- return 0
132
- else:
133
- return intersection_area / min_box_area
134
-
135
-
136
- def calculate_iou(bbox1, bbox2):
137
- """计算两个边界框的交并比(IOU)。
138
-
139
- Args:
140
- bbox1 (list[float]): 第一个边界框的坐标,格式为 [x1, y1, x2, y2],其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
141
- bbox2 (list[float]): 第二个边界框的坐标,格式与 `bbox1` 相同。
142
-
143
- Returns:
144
- float: 两个边界框的交并比(IOU),取值范围为 [0, 1]。
145
- """
146
- # Determine the coordinates of the intersection rectangle
147
- x_left = max(bbox1[0], bbox2[0])
148
- y_top = max(bbox1[1], bbox2[1])
149
- x_right = min(bbox1[2], bbox2[2])
150
- y_bottom = min(bbox1[3], bbox2[3])
151
-
152
- if x_right < x_left or y_bottom < y_top:
153
- return 0.0
154
-
155
- # The area of overlap area
156
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
157
-
158
- # The area of both rectangles
159
- bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
160
- bbox2_area = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
161
-
162
- if any([bbox1_area == 0, bbox2_area == 0]):
163
- return 0
164
-
165
- # Compute the intersection over union by taking the intersection area
166
- # and dividing it by the sum of both areas minus the intersection area
167
- iou = intersection_area / float(bbox1_area + bbox2_area - intersection_area)
168
-
169
- return iou
170
-
171
-
172
- def calculate_overlap_area_in_bbox1_area_ratio(bbox1, bbox2):
173
- """计算box1和box2的重叠面积占bbox1的比例."""
174
- # Determine the coordinates of the intersection rectangle
175
- x_left = max(bbox1[0], bbox2[0])
176
- y_top = max(bbox1[1], bbox2[1])
177
- x_right = min(bbox1[2], bbox2[2])
178
- y_bottom = min(bbox1[3], bbox2[3])
179
-
180
- if x_right < x_left or y_bottom < y_top:
181
- return 0.0
182
-
183
- # The area of overlap area
184
- intersection_area = (x_right - x_left) * (y_bottom - y_top)
185
- bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
186
- if bbox1_area == 0:
187
- return 0
188
- else:
189
- return intersection_area / bbox1_area
190
-
191
-
192
- def calculate_vertical_projection_overlap_ratio(block1, block2):
193
- """
194
- Calculate the proportion of the x-axis covered by the vertical projection of two blocks.
195
-
196
- Args:
197
- block1 (tuple): Coordinates of the first block (x0, y0, x1, y1).
198
- block2 (tuple): Coordinates of the second block (x0, y0, x1, y1).
199
-
200
- Returns:
201
- float: The proportion of the x-axis covered by the vertical projection of the two blocks.
202
- """
203
- x0_1, _, x1_1, _ = block1
204
- x0_2, _, x1_2, _ = block2
205
-
206
- # Calculate the intersection of the x-coordinates
207
- x_left = max(x0_1, x0_2)
208
- x_right = min(x1_1, x1_2)
209
-
210
- if x_right < x_left:
211
- return 0.0
212
-
213
- # Length of the intersection
214
- intersection_length = x_right - x_left
215
-
216
- # Length of the x-axis projection of the first block
217
- block1_length = x1_1 - x0_1
218
-
219
- if block1_length == 0:
220
- return 0.0
221
-
222
- # Proportion of the x-axis covered by the intersection
223
- # logger.info(f"intersection_length: {intersection_length}, block1_length: {block1_length}")
224
- return intersection_length / block1_length
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/char_utils.py DELETED
@@ -1,55 +0,0 @@
1
- # Copyright (c) Opendatalab. All rights reserved.
2
- import re
3
-
4
-
5
- def is_hyphen_at_line_end(line):
6
- """Check if a line ends with one or more letters followed by a hyphen.
7
-
8
- Args:
9
- line (str): The line of text to check.
10
-
11
- Returns:
12
- bool: True if the line ends with one or more letters followed by a hyphen, False otherwise.
13
- """
14
- # Use regex to check if the line ends with one or more letters followed by a hyphen
15
- return bool(re.search(r'[A-Za-z]+-\s*$', line))
16
-
17
-
18
- def full_to_half_exclude_marks(text: str) -> str:
19
- """Convert full-width characters to half-width characters using code point manipulation.
20
-
21
- Args:
22
- text: String containing full-width characters
23
-
24
- Returns:
25
- String with full-width characters converted to half-width
26
- """
27
- result = []
28
- for char in text:
29
- code = ord(char)
30
- # Full-width letters and numbers (FF21-FF3A for A-Z, FF41-FF5A for a-z, FF10-FF19 for 0-9)
31
- if (0xFF21 <= code <= 0xFF3A) or (0xFF41 <= code <= 0xFF5A) or (0xFF10 <= code <= 0xFF19):
32
- result.append(chr(code - 0xFEE0)) # Shift to ASCII range
33
- else:
34
- result.append(char)
35
- return ''.join(result)
36
-
37
-
38
- def full_to_half(text: str) -> str:
39
- """Convert full-width characters to half-width characters using code point manipulation.
40
-
41
- Args:
42
- text: String containing full-width characters
43
-
44
- Returns:
45
- String with full-width characters converted to half-width
46
- """
47
- result = []
48
- for char in text:
49
- code = ord(char)
50
- # Full-width letters, numbers and punctuation (FF01-FF5E)
51
- if 0xFF01 <= code <= 0xFF5E:
52
- result.append(chr(code - 0xFEE0)) # Shift to ASCII range
53
- else:
54
- result.append(char)
55
- return ''.join(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/check_sys_env.py DELETED
@@ -1,38 +0,0 @@
1
-
2
- import platform
3
-
4
- from packaging import version
5
-
6
-
7
- def is_windows_environment() -> bool:
8
- return platform.system() == "Windows"
9
-
10
-
11
- # Detect if the current environment is a Mac computer
12
- def is_mac_environment() -> bool:
13
- return platform.system() == "Darwin"
14
-
15
-
16
- def is_linux_environment() -> bool:
17
- return platform.system() == "Linux"
18
-
19
-
20
- # Detect if CPU is Apple Silicon architecture
21
- def is_apple_silicon_cpu() -> bool:
22
- return platform.machine() in ["arm64", "aarch64"]
23
-
24
-
25
- # If Mac computer with Apple Silicon architecture, check if macOS version is 13.5 or above
26
- def is_mac_os_version_supported(min_version: str = "13.5") -> bool:
27
- if not is_mac_environment() or not is_apple_silicon_cpu():
28
- return False
29
- mac_version = platform.mac_ver()[0]
30
- if not mac_version:
31
- return False
32
- # print("Mac OS Version:", mac_version)
33
- return version.parse(mac_version) >= version.parse(min_version)
34
-
35
- if __name__ == "__main__":
36
- print("Is Mac Environment:", is_mac_environment())
37
- print("Is Apple Silicon CPU:", is_apple_silicon_cpu())
38
- print("Is Mac OS Version Supported (>=13.5):", is_mac_os_version_supported())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/config_reader.py DELETED
@@ -1,136 +0,0 @@
1
-
2
- import json
3
- import os
4
- from loguru import logger
5
-
6
- try:
7
- import torch
8
- import torch_npu
9
- except ImportError:
10
- pass
11
-
12
-
13
- # 定义配置文件名常量
14
- CONFIG_FILE_NAME = os.getenv('NaviOCR_TOOLS_CONFIG_JSON', 'NaviOCR.json')
15
-
16
-
17
- def read_config():
18
- if os.path.isabs(CONFIG_FILE_NAME):
19
- config_file = CONFIG_FILE_NAME
20
- else:
21
- home_dir = os.path.expanduser('~')
22
- config_file = os.path.join(home_dir, CONFIG_FILE_NAME)
23
-
24
- if not os.path.exists(config_file):
25
- # logger.warning(f'{config_file} not found, using default configuration')
26
- return None
27
- else:
28
- with open(config_file, 'r', encoding='utf-8') as f:
29
- config = json.load(f)
30
- return config
31
-
32
-
33
- def get_s3_config(bucket_name: str):
34
- """~/magic-pdf.json 读出来."""
35
- config = read_config()
36
-
37
- bucket_info = config.get('bucket_info')
38
- if bucket_name not in bucket_info:
39
- access_key, secret_key, storage_endpoint = bucket_info['[default]']
40
- else:
41
- access_key, secret_key, storage_endpoint = bucket_info[bucket_name]
42
-
43
- if access_key is None or secret_key is None or storage_endpoint is None:
44
- raise Exception(f'ak, sk or endpoint not found in {CONFIG_FILE_NAME}')
45
-
46
- # logger.info(f"get_s3_config: ak={access_key}, sk={secret_key}, endpoint={storage_endpoint}")
47
-
48
- return access_key, secret_key, storage_endpoint
49
-
50
-
51
- def get_s3_config_dict(path: str):
52
- access_key, secret_key, storage_endpoint = get_s3_config(get_bucket_name(path))
53
- return {'ak': access_key, 'sk': secret_key, 'endpoint': storage_endpoint}
54
-
55
-
56
- def get_bucket_name(path):
57
- bucket, key = parse_bucket_key(path)
58
- return bucket
59
-
60
-
61
- def parse_bucket_key(s3_full_path: str):
62
- """
63
- 输入 s3://bucket/path/to/my/file.txt
64
- 输出 bucket, path/to/my/file.txt
65
- """
66
- s3_full_path = s3_full_path.strip()
67
- if s3_full_path.startswith("s3://"):
68
- s3_full_path = s3_full_path[5:]
69
- if s3_full_path.startswith("/"):
70
- s3_full_path = s3_full_path[1:]
71
- bucket, key = s3_full_path.split("/", 1)
72
- return bucket, key
73
-
74
-
75
- def get_device():
76
- device_mode = os.getenv('NaviOCR_DEVICE_MODE', None)
77
- if device_mode is not None:
78
- return device_mode
79
- else:
80
- if torch.cuda.is_available():
81
- return "cuda"
82
- elif torch.backends.mps.is_available():
83
- return "mps"
84
- else:
85
- try:
86
- if torch_npu.npu.is_available():
87
- return "npu"
88
- except Exception as e:
89
- pass
90
- return "cpu"
91
-
92
-
93
- def get_formula_enable(formula_enable):
94
- formula_enable_env = os.getenv('NaviOCR_FORMULA_ENABLE')
95
- formula_enable = formula_enable if formula_enable_env is None else formula_enable_env.lower() == 'true'
96
- return formula_enable
97
-
98
-
99
- def get_table_enable(table_enable):
100
- table_enable_env = os.getenv('NaviOCR_TABLE_ENABLE')
101
- table_enable = table_enable if table_enable_env is None else table_enable_env.lower() == 'true'
102
- return table_enable
103
-
104
-
105
- def get_latex_delimiter_config():
106
- config = read_config()
107
- if config is None:
108
- return None
109
- latex_delimiter_config = config.get('latex-delimiter-config', None)
110
- if latex_delimiter_config is None:
111
- # logger.warning(f"'latex-delimiter-config' not found in {CONFIG_FILE_NAME}, use 'None' as default")
112
- return None
113
- else:
114
- return latex_delimiter_config
115
-
116
-
117
- def get_llm_aided_config():
118
- config = read_config()
119
- if config is None:
120
- return None
121
- llm_aided_config = config.get('llm-aided-config', None)
122
- if llm_aided_config is None:
123
- # logger.warning(f"'llm-aided-config' not found in {CONFIG_FILE_NAME}, use 'None' as default")
124
- return None
125
- else:
126
- return llm_aided_config
127
-
128
-
129
- def get_local_models_dir():
130
- config = read_config()
131
- if config is None:
132
- return None
133
- models_dir = config.get('models-dir')
134
- if models_dir is None:
135
- logger.warning(f"'models-dir' not found in {CONFIG_FILE_NAME}, use None as default")
136
- return models_dir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/cut_image.py DELETED
@@ -1,36 +0,0 @@
1
- from loguru import logger
2
-
3
- from .pdf_image_tools import cut_image
4
- import numpy as np
5
-
6
- def merge_bboxes_if_needed(bboxes):
7
- if len(bboxes) > 4:
8
- bboxes = np.array(bboxes).reshape(-1, 2)
9
- minx = int(bboxes[:, 0].min())
10
- miny = int(bboxes[:, 1].min())
11
- maxx = int(bboxes[:, 0].max())
12
- maxy = int(bboxes[:, 1].max())
13
- return [minx, miny, maxx, maxy]
14
- return [int(x) for x in bboxes]
15
-
16
- def cut_image_and_table(span, page_pil_img, page_img_md5, page_id, image_writer, scale=2):
17
- def return_path(path_type):
18
- return f"{path_type}/{page_img_md5}"
19
-
20
- span_type = span["type"]
21
- span["bbox"] = merge_bboxes_if_needed(span["bbox"])
22
-
23
- if not check_img_bbox(span["bbox"]) or not image_writer:
24
- span["image_path"] = ""
25
- else:
26
- span["image_path"] = cut_image(
27
- span["bbox"], page_id, page_pil_img, return_path=return_path(span_type), image_writer=image_writer, scale=scale
28
- )
29
- return span
30
-
31
-
32
- def check_img_bbox(bbox) -> bool:
33
- if any([bbox[0] >= bbox[2], bbox[1] >= bbox[3]]):
34
- logger.warning(f"image_bboxes: 错误的box, {bbox}")
35
- return False
36
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/draw_bbox.py DELETED
@@ -1,634 +0,0 @@
1
- import json
2
- from io import BytesIO
3
-
4
- from loguru import logger
5
- from pypdf import PdfReader, PdfWriter, PageObject
6
- from reportlab.pdfgen import canvas
7
-
8
- from .enum_class import BlockType, ContentType, SplitFlag
9
-
10
-
11
- def cal_canvas_quad(page, bbox):
12
- page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
13
- actual_width = page_width
14
- actual_height = page_height
15
- rotation_obj = page.get("/Rotate", 0)
16
- try:
17
- rotation = int(rotation_obj) % 360
18
- except (ValueError, TypeError) as e:
19
- logger.warning(f"Invalid /Rotate value {rotation_obj!r}; defaulting to 0. Error: {e}")
20
- rotation = 0
21
- if rotation in [90, 270]:
22
- actual_width, actual_height = actual_height, actual_width
23
-
24
- pts = []
25
- for i in range(0, len(bbox), 2):
26
- x = bbox[i]
27
- y = bbox[i + 1]
28
- if rotation == 270:
29
- x, y = actual_height - y, actual_width - x
30
- elif rotation == 180:
31
- x = page_width - x
32
- elif rotation == 90:
33
- x, y = y, x
34
- else:
35
- y = page_height - y
36
- pts.extend([x, y])
37
- return pts
38
-
39
-
40
- def cal_canvas_rect(page, bbox):
41
- """
42
- Calculate the rectangle coordinates on the canvas based on the original PDF page and bounding box.
43
-
44
- Args:
45
- page: A PyPDF2 Page object representing a single page in the PDF.
46
- bbox: [x0, y0, x1, y1] representing the bounding box coordinates.
47
-
48
- Returns:
49
- rect: [x0, y0, width, height] representing the rectangle coordinates on the canvas.
50
- """
51
- page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
52
-
53
- actual_width = page_width # The width of the final PDF display
54
- actual_height = page_height # The height of the final PDF display
55
-
56
- rotation_obj = page.get("/Rotate", 0)
57
- try:
58
- rotation = int(rotation_obj) % 360 # cast rotation to int to handle IndirectObject
59
- except (ValueError, TypeError) as e:
60
- logger.warning(f"Invalid /Rotate value {rotation_obj!r} on page; defaulting to 0. Error: {e}")
61
- rotation = 0
62
-
63
- if rotation in [90, 270]:
64
- # PDF is rotated 90 degrees or 270 degrees, and the width and height need to be swapped
65
- actual_width, actual_height = actual_height, actual_width
66
-
67
- x0, y0, x1, y1 = bbox
68
- rect_w = abs(x1 - x0)
69
- rect_h = abs(y1 - y0)
70
-
71
- if rotation == 270:
72
- rect_w, rect_h = rect_h, rect_w
73
- x0 = actual_height - y1
74
- y0 = actual_width - x1
75
- elif rotation == 180:
76
- x0 = page_width - x1
77
- # y0 stays the same
78
- elif rotation == 90:
79
- rect_w, rect_h = rect_h, rect_w
80
- x0, y0 = y0, x0
81
- else:
82
- # rotation == 0
83
- y0 = page_height - y1
84
-
85
- rect = [x0, y0, rect_w, rect_h]
86
- return rect
87
-
88
-
89
- def draw_bbox_without_number(i, bbox_list, page, c, rgb_config, fill_config):
90
- new_rgb = [float(color) / 255 for color in rgb_config]
91
- page_data = bbox_list[i]
92
-
93
- for bbox in page_data:
94
- # ---------- 2点框 ----------
95
- if len(bbox) == 4:
96
- rect = cal_canvas_rect(page, bbox)
97
-
98
- if fill_config:
99
- c.setFillColorRGB(new_rgb[0], new_rgb[1], new_rgb[2], 0.3)
100
- c.rect(rect[0], rect[1], rect[2], rect[3], stroke=0, fill=1)
101
- else:
102
- c.setStrokeColorRGB(new_rgb[0], new_rgb[1], new_rgb[2])
103
- c.rect(rect[0], rect[1], rect[2], rect[3], stroke=1, fill=0)
104
- # ---------- 多点框 ----------
105
- elif len(bbox) % 2 == 0:
106
- pts = cal_canvas_quad(page, bbox)
107
- path = c.beginPath()
108
- path.moveTo(pts[0], pts[1])
109
- for j in range(2, len(pts), 2):
110
- path.lineTo(pts[j], pts[j + 1])
111
- path.close()
112
- if fill_config:
113
- c.setFillColorRGB(*new_rgb, 0.3)
114
- c.drawPath(path, stroke=0, fill=1)
115
- else:
116
- c.setStrokeColorRGB(*new_rgb)
117
- c.drawPath(path, stroke=1, fill=0)
118
- else:
119
- logger.warning(f"Invalid bbox length: {bbox}")
120
- continue
121
-
122
- return c
123
-
124
-
125
- def draw_bbox_with_number(i, bbox_list, page, c, rgb_config, fill_config, draw_bbox=True):
126
- new_rgb = [float(color) / 255 for color in rgb_config]
127
- page_data = bbox_list[i]
128
-
129
- page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
130
-
131
- for j, (bbox, angle) in enumerate(page_data):
132
-
133
- # ---------- 2点框 ----------
134
- if len(bbox) == 4:
135
- rect = cal_canvas_rect(page, bbox)
136
-
137
- if draw_bbox:
138
- if fill_config:
139
- c.setFillColorRGB(*new_rgb, 0.3)
140
- c.rect(rect[0], rect[1], rect[2], rect[3], stroke=0, fill=1)
141
- else:
142
- c.setStrokeColorRGB(*new_rgb)
143
- c.rect(rect[0], rect[1], rect[2], rect[3], stroke=1, fill=0)
144
-
145
- # ---------- 多点框 ----------
146
- elif len(bbox) % 2 == 0:
147
- pts = cal_canvas_quad(page, bbox)
148
-
149
- xs = pts[0::2]
150
- ys = pts[1::2]
151
-
152
- rect = [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)]
153
-
154
- if draw_bbox:
155
- path = c.beginPath()
156
- path.moveTo(pts[0], pts[1])
157
- for i in range(2, len(pts), 2):
158
- path.lineTo(pts[i], pts[i + 1])
159
- path.close()
160
- if fill_config:
161
- c.setFillColorRGB(*new_rgb, 0.3)
162
- c.drawPath(path, stroke=0, fill=1)
163
- else:
164
- c.setStrokeColorRGB(*new_rgb)
165
- c.drawPath(path, stroke=1, fill=0)
166
- else:
167
- logger.warning(f"Invalid bbox length: {bbox}")
168
- continue
169
- c.setFillColorRGB(*new_rgb, 1.0)
170
- c.setFontSize(size=10)
171
-
172
- c.saveState()
173
-
174
- rotation_obj = page.get("/Rotate", 0)
175
- try:
176
- rotation = int(rotation_obj) % 360
177
- except (ValueError, TypeError):
178
- logger.warning(f"Invalid /Rotate value: {rotation_obj!r}, defaulting to 0")
179
- rotation = 0
180
-
181
- if rotation == 0:
182
- c.translate(rect[0] + rect[2] + 2, rect[1] + rect[3] - 10)
183
- elif rotation == 90:
184
- c.translate(rect[0] + 10, rect[1] + rect[3] + 2)
185
- elif rotation == 180:
186
- c.translate(rect[0] - 2, rect[1] + 10)
187
- elif rotation == 270:
188
- c.translate(rect[0] + rect[2] - 10, rect[1] - 2)
189
-
190
- c.rotate(rotation)
191
- c.drawString(0, 0, str(j + 1) + '+' + str(angle))
192
-
193
- c.restoreState()
194
-
195
- return c
196
-
197
-
198
- def draw_layout_bbox(pdf_info, pdf_bytes, out_path, filename):
199
- dropped_bbox_list = []
200
-
201
- tables_body_list, tables_caption_list, tables_footnote_list = [], [], []
202
- imgs_body_list, imgs_caption_list, imgs_footnote_list = [], [], []
203
- codes_body_list, codes_caption_list = [], []
204
-
205
- titles_list = []
206
- texts_list = []
207
- interequations_list = []
208
- lists_list = []
209
- list_items_list = []
210
- indexs_list = []
211
- seal_list = []
212
- char_list = []
213
-
214
- for page in pdf_info:
215
- page_dropped_list = []
216
-
217
- tables_body, tables_caption, tables_footnote = [], [], []
218
- imgs_body, imgs_caption, imgs_footnote = [], [], []
219
- codes_body, codes_caption = [], []
220
-
221
- titles = []
222
- texts = []
223
- interequations = []
224
- lists = []
225
- list_items = []
226
- indices = []
227
- seals = []
228
- chars = []
229
-
230
- for dropped_bbox in page['discarded_blocks']:
231
- page_dropped_list.append(dropped_bbox['bbox'])
232
-
233
- dropped_bbox_list.append(page_dropped_list)
234
-
235
- for block in page["para_blocks"]:
236
- block_bbox = block["bbox"]
237
-
238
- if block["type"] == BlockType.TABLE:
239
- for nested_block in block.get("blocks", []):
240
- nested_bbox = nested_block["bbox"]
241
-
242
- if nested_block["type"] == BlockType.TABLE_BODY:
243
- tables_body.append(nested_bbox)
244
-
245
- elif nested_block["type"] == BlockType.TABLE_CAPTION:
246
- tables_caption.append(nested_bbox)
247
-
248
- elif nested_block["type"] == BlockType.TABLE_FOOTNOTE:
249
- if nested_block.get(SplitFlag.CROSS_PAGE, False):
250
- continue
251
- tables_footnote.append(nested_bbox)
252
-
253
- elif block["type"] == BlockType.IMAGE:
254
- for nested_block in block.get("blocks", []):
255
- nested_bbox = nested_block["bbox"]
256
-
257
- if nested_block["type"] == BlockType.IMAGE_BODY:
258
- imgs_body.append(nested_bbox)
259
-
260
- elif nested_block["type"] == BlockType.IMAGE_CAPTION:
261
- imgs_caption.append(nested_bbox)
262
-
263
- elif nested_block["type"] == BlockType.IMAGE_FOOTNOTE:
264
- imgs_footnote.append(nested_bbox)
265
-
266
- elif block["type"] == BlockType.CODE:
267
- for nested_block in block.get("blocks", []):
268
- nested_bbox = nested_block["bbox"]
269
-
270
- if nested_block["type"] == BlockType.CODE_BODY:
271
- codes_body.append(nested_bbox)
272
-
273
- elif nested_block["type"] == BlockType.CODE_CAPTION:
274
- codes_caption.append(nested_bbox)
275
-
276
- elif block["type"] == BlockType.TITLE:
277
- titles.append(block_bbox)
278
-
279
- elif block["type"] in [BlockType.TEXT, BlockType.REF_TEXT]:
280
- texts.append(block_bbox)
281
-
282
- elif block["type"] == BlockType.INTERLINE_EQUATION:
283
- interequations.append(block_bbox)
284
-
285
- elif block["type"] == BlockType.SEAL:
286
- seals.append(block_bbox)
287
-
288
- elif block["type"] == BlockType.CHAR:
289
- chars.append(block_bbox)
290
-
291
- elif block["type"] == BlockType.LIST:
292
- lists.append(block_bbox)
293
-
294
- if "blocks" in block:
295
- for sub_block in block["blocks"]:
296
- list_items.append(sub_block["bbox"])
297
-
298
- elif block["type"] == BlockType.INDEX:
299
- indices.append(block_bbox)
300
-
301
- tables_body_list.append(tables_body)
302
- tables_caption_list.append(tables_caption)
303
- tables_footnote_list.append(tables_footnote)
304
-
305
- imgs_body_list.append(imgs_body)
306
- imgs_caption_list.append(imgs_caption)
307
- imgs_footnote_list.append(imgs_footnote)
308
-
309
- titles_list.append(titles)
310
- texts_list.append(texts)
311
- interequations_list.append(interequations)
312
-
313
- seal_list.append(seals)
314
- char_list.append(chars)
315
- lists_list.append(lists)
316
- list_items_list.append(list_items)
317
-
318
- indexs_list.append(indices)
319
-
320
- codes_body_list.append(codes_body)
321
- codes_caption_list.append(codes_caption)
322
-
323
- layout_bbox_list = []
324
-
325
- table_type_order = {
326
- "table_caption": 1,
327
- "table_body": 2,
328
- "table_footnote": 3
329
- }
330
-
331
- for page in pdf_info:
332
- page_block_list = []
333
-
334
- for block in page["para_blocks"]:
335
- if block["type"] in [
336
- BlockType.TEXT,
337
- BlockType.REF_TEXT,
338
- BlockType.TITLE,
339
- BlockType.INTERLINE_EQUATION,
340
- BlockType.SEAL,
341
- BlockType.CHAR,
342
- BlockType.LIST,
343
- BlockType.INDEX,
344
- ]:
345
- bbox = block["bbox"]
346
- page_block_list.append([bbox, block["angle"]])
347
-
348
- elif block["type"] == BlockType.IMAGE:
349
- for sub_block in block.get("blocks", []):
350
- bbox = sub_block["bbox"]
351
- page_block_list.append([bbox, sub_block["angle"]])
352
-
353
- elif block["type"] == BlockType.TABLE:
354
- sorted_blocks = sorted(
355
- block.get("blocks", []),
356
- key=lambda x: table_type_order[x["type"]]
357
- )
358
-
359
- for sub_block in sorted_blocks:
360
- if sub_block.get(SplitFlag.CROSS_PAGE, False):
361
- continue
362
-
363
- bbox = sub_block["bbox"]
364
- page_block_list.append([bbox, sub_block["angle"]])
365
-
366
- elif block["type"] == BlockType.CODE:
367
- for sub_block in block.get("blocks", []):
368
- bbox = sub_block["bbox"]
369
- page_block_list.append([bbox, sub_block["angle"]])
370
-
371
- layout_bbox_list.append(page_block_list)
372
-
373
- pdf_bytes_io = BytesIO(pdf_bytes)
374
- pdf_docs = PdfReader(pdf_bytes_io)
375
- output_pdf = PdfWriter()
376
-
377
- for i, page in enumerate(pdf_docs.pages):
378
- page_width = float(page.cropbox[2])
379
- page_height = float(page.cropbox[3])
380
-
381
- custom_page_size = (page_width, page_height)
382
-
383
- packet = BytesIO()
384
- c = canvas.Canvas(packet, pagesize=custom_page_size)
385
-
386
- c = draw_bbox_without_number(i, codes_body_list, page, c, [102, 0, 204], True)
387
- c = draw_bbox_without_number(i, codes_caption_list, page, c, [204, 153, 255], True)
388
- c = draw_bbox_without_number(i, dropped_bbox_list, page, c, [158, 158, 158], True)
389
-
390
- c = draw_bbox_without_number(i, tables_body_list, page, c, [204, 204, 0], True)
391
- c = draw_bbox_without_number(i, tables_caption_list, page, c, [255, 255, 102], True)
392
- c = draw_bbox_without_number(i, tables_footnote_list, page, c, [229, 255, 204], True)
393
-
394
- c = draw_bbox_without_number(i, imgs_body_list, page, c, [153, 255, 51], True)
395
- c = draw_bbox_without_number(i, imgs_caption_list, page, c, [102, 178, 255], True)
396
- c = draw_bbox_without_number(i, imgs_footnote_list, page, c, [255, 178, 102], True)
397
-
398
- c = draw_bbox_without_number(i, titles_list, page, c, [102, 102, 255], True)
399
- c = draw_bbox_without_number(i, texts_list, page, c, [153, 0, 76], True)
400
-
401
- c = draw_bbox_without_number(i, interequations_list, page, c, [0, 255, 0], True)
402
- c = draw_bbox_without_number(i, seal_list, page, c, [255, 0, 255], True)
403
- c = draw_bbox_without_number(i, char_list, page, c, [255, 100, 255], True)
404
-
405
- c = draw_bbox_without_number(i, lists_list, page, c, [40, 169, 92], True)
406
- c = draw_bbox_without_number(i, list_items_list, page, c, [40, 169, 92], False)
407
-
408
- c = draw_bbox_without_number(i, indexs_list, page, c, [40, 169, 92], True)
409
-
410
- c = draw_bbox_with_number(
411
- i,
412
- layout_bbox_list,
413
- page,
414
- c,
415
- [255, 0, 0],
416
- False,
417
- draw_bbox=False
418
- )
419
-
420
- c.save()
421
-
422
- packet.seek(0)
423
- overlay_pdf = PdfReader(packet)
424
-
425
- if len(overlay_pdf.pages) > 0:
426
- new_page = PageObject(pdf=None)
427
- new_page.update(page)
428
-
429
- page = new_page
430
- page.merge_page(overlay_pdf.pages[0])
431
-
432
- output_pdf.add_page(page)
433
-
434
- with open(f"{out_path}/{filename}", "wb") as f:
435
- output_pdf.write(f)
436
-
437
- def draw_span_bbox(pdf_info, pdf_bytes, out_path, filename):
438
- text_list = []
439
- inline_equation_list = []
440
- interline_equation_list = []
441
- image_list = []
442
- table_list = []
443
- dropped_list = []
444
-
445
- def get_span_info(span):
446
- if span['type'] == ContentType.TEXT:
447
- page_text_list.append(span['bbox'])
448
- elif span['type'] == ContentType.INLINE_EQUATION:
449
- page_inline_equation_list.append(span['bbox'])
450
- elif span['type'] == ContentType.INTERLINE_EQUATION:
451
- page_interline_equation_list.append(span['bbox'])
452
- elif span['type'] == ContentType.IMAGE:
453
- page_image_list.append(span['bbox'])
454
- elif span['type'] == ContentType.TABLE:
455
- page_table_list.append(span['bbox'])
456
-
457
- for page in pdf_info:
458
- page_text_list = []
459
- page_inline_equation_list = []
460
- page_interline_equation_list = []
461
- page_image_list = []
462
- page_table_list = []
463
- page_dropped_list = []
464
-
465
-
466
- # 构造dropped_list
467
- for block in page['discarded_blocks']:
468
- if block['type'] == BlockType.DISCARDED:
469
- for line in block['lines']:
470
- for span in line['spans']:
471
- page_dropped_list.append(span['bbox'])
472
- dropped_list.append(page_dropped_list)
473
- # 构造其余useful_list
474
- for block in page['para_blocks']: # span直接用分段合并前的结果就可以
475
- # for block in page['preproc_blocks']:
476
- # print(block.keys(), block['type'])
477
- if block['type'] in [
478
- BlockType.TEXT,
479
- BlockType.TITLE,
480
- BlockType.INTERLINE_EQUATION,
481
- BlockType.INDEX,
482
- ]:
483
- for line in block['lines']:
484
- for span in line['spans']:
485
- get_span_info(span)
486
- elif block['type'] in [BlockType.IMAGE, BlockType.TABLE, BlockType.LIST]:
487
- for sub_block in block['blocks']:
488
- for line in sub_block['lines']:
489
- for span in line['spans']:
490
- get_span_info(span)
491
- text_list.append(page_text_list)
492
- inline_equation_list.append(page_inline_equation_list)
493
- interline_equation_list.append(page_interline_equation_list)
494
- image_list.append(page_image_list)
495
- table_list.append(page_table_list)
496
-
497
- pdf_bytes_io = BytesIO(pdf_bytes)
498
- pdf_docs = PdfReader(pdf_bytes_io)
499
- output_pdf = PdfWriter()
500
-
501
- for i, page in enumerate(pdf_docs.pages):
502
- # 获取原始页面尺寸
503
- page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
504
- custom_page_size = (page_width, page_height)
505
-
506
- packet = BytesIO()
507
- # 使用原始PDF的尺寸创建canvas
508
- c = canvas.Canvas(packet, pagesize=custom_page_size)
509
-
510
- # 获取当前页面的数据
511
- draw_bbox_without_number(i, text_list, page, c,[255, 0, 0], False)
512
- draw_bbox_without_number(i, inline_equation_list, page, c, [0, 255, 0], False)
513
- draw_bbox_without_number(i, interline_equation_list, page, c, [0, 0, 255], False)
514
- draw_bbox_without_number(i, image_list, page, c, [255, 204, 0], False)
515
- draw_bbox_without_number(i, table_list, page, c, [204, 0, 255], False)
516
- draw_bbox_without_number(i, dropped_list, page, c, [158, 158, 158], False)
517
-
518
- c.save()
519
- packet.seek(0)
520
- overlay_pdf = PdfReader(packet)
521
-
522
- # 添加检查确保overlay_pdf.pages不为空
523
- if len(overlay_pdf.pages) > 0:
524
- new_page = PageObject(pdf=None)
525
- new_page.update(page)
526
- page = new_page
527
- page.merge_page(overlay_pdf.pages[0])
528
- else:
529
- # 记录日志并继续处理下一个页面
530
- # logger.warning(f"span.pdf: 第{i + 1}页未能生成有效的overlay PDF")
531
- pass
532
-
533
- output_pdf.add_page(page)
534
-
535
- # Save the PDF
536
- with open(f"{out_path}/{filename}", "wb") as f:
537
- output_pdf.write(f)
538
-
539
-
540
- def draw_line_sort_bbox(pdf_info, pdf_bytes, out_path, filename):
541
- layout_bbox_list = []
542
-
543
- for page in pdf_info:
544
- page_line_list = []
545
- for block in page['preproc_blocks']:
546
- if block['type'] in [BlockType.TEXT]:
547
- for line in block['lines']:
548
- bbox = line['bbox']
549
- index = line['index']
550
- page_line_list.append({'index': index, 'bbox': bbox})
551
- elif block['type'] in [BlockType.TITLE, BlockType.INTERLINE_EQUATION]:
552
- if 'virtual_lines' in block:
553
- if len(block['virtual_lines']) > 0 and block['virtual_lines'][0].get('index', None) is not None:
554
- for line in block['virtual_lines']:
555
- bbox = line['bbox']
556
- index = line['index']
557
- page_line_list.append({'index': index, 'bbox': bbox})
558
- else:
559
- for line in block['lines']:
560
- bbox = line['bbox']
561
- index = line['index']
562
- page_line_list.append({'index': index, 'bbox': bbox})
563
- elif block['type'] in [BlockType.IMAGE, BlockType.TABLE]:
564
- for sub_block in block['blocks']:
565
- if sub_block['type'] in [BlockType.IMAGE_BODY, BlockType.TABLE_BODY]:
566
- if len(sub_block['virtual_lines']) > 0 and sub_block['virtual_lines'][0].get('index', None) is not None:
567
- for line in sub_block['virtual_lines']:
568
- bbox = line['bbox']
569
- index = line['index']
570
- page_line_list.append({'index': index, 'bbox': bbox})
571
- else:
572
- for line in sub_block['lines']:
573
- bbox = line['bbox']
574
- index = line['index']
575
- page_line_list.append({'index': index, 'bbox': bbox})
576
- elif sub_block['type'] in [BlockType.IMAGE_CAPTION, BlockType.TABLE_CAPTION, BlockType.IMAGE_FOOTNOTE, BlockType.TABLE_FOOTNOTE]:
577
- for line in sub_block['lines']:
578
- bbox = line['bbox']
579
- index = line['index']
580
- page_line_list.append({'index': index, 'bbox': bbox})
581
- sorted_bboxes = sorted(page_line_list, key=lambda x: x['index'])
582
- layout_bbox_list.append(sorted_bbox['bbox'] for sorted_bbox in sorted_bboxes)
583
- pdf_bytes_io = BytesIO(pdf_bytes)
584
- pdf_docs = PdfReader(pdf_bytes_io)
585
- output_pdf = PdfWriter()
586
-
587
- for i, page in enumerate(pdf_docs.pages):
588
- # 获取原始页面尺寸
589
- page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
590
- custom_page_size = (page_width, page_height)
591
-
592
- packet = BytesIO()
593
- # 使用原始PDF的尺寸创建canvas
594
- c = canvas.Canvas(packet, pagesize=custom_page_size)
595
-
596
- # 获取当前页面的数据
597
- draw_bbox_with_number(i, layout_bbox_list, page, c, [255, 0, 0], False)
598
-
599
- c.save()
600
- packet.seek(0)
601
- overlay_pdf = PdfReader(packet)
602
-
603
- # 添加检查确保overlay_pdf.pages不为空
604
- if len(overlay_pdf.pages) > 0:
605
- new_page = PageObject(pdf=None)
606
- new_page.update(page)
607
- page = new_page
608
- page.merge_page(overlay_pdf.pages[0])
609
- else:
610
- # 记录日志并继续处理下一个页面
611
- # logger.warning(f"span.pdf: 第{i + 1}页未能生成有效的overlay PDF")
612
- pass
613
-
614
- output_pdf.add_page(page)
615
-
616
- # Save the PDF
617
- with open(f"{out_path}/{filename}", "wb") as f:
618
- output_pdf.write(f)
619
-
620
-
621
- if __name__ == "__main__":
622
- # 读取PDF文件
623
- pdf_path = "examples/demo1.pdf"
624
- with open(pdf_path, "rb") as f:
625
- pdf_bytes = f.read()
626
-
627
- # 从json文件读取pdf_info
628
-
629
- json_path = "examples/demo1_1746005777.0863056_middle.json"
630
- with open(json_path, "r", encoding="utf-8") as f:
631
- pdf_ann = json.load(f)
632
- pdf_info = pdf_ann["pdf_info"]
633
- # 调用可视化函数,输出到examples目录
634
- draw_layout_bbox(pdf_info, pdf_bytes, "examples", "output_with_layout.pdf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/enum_class.py DELETED
@@ -1,67 +0,0 @@
1
- from enum import Enum
2
-
3
- class BlockType:
4
- IMAGE = 'image'
5
- TABLE = 'table'
6
- IMAGE_BODY = 'image_body'
7
- TABLE_BODY = 'table_body'
8
- IMAGE_CAPTION = 'image_caption'
9
- TABLE_CAPTION = 'table_caption'
10
- IMAGE_FOOTNOTE = 'image_footnote'
11
- TABLE_FOOTNOTE = 'table_footnote'
12
- TEXT = 'text'
13
- TITLE = 'title'
14
- INTERLINE_EQUATION = 'interline_equation'
15
- LIST = 'list'
16
- INDEX = 'index'
17
- DISCARDED = 'discarded'
18
-
19
- CODE = "code"
20
- CODE_BODY = "code_body"
21
- CODE_CAPTION = "code_caption"
22
- ALGORITHM = "algorithm"
23
- REF_TEXT = "ref_text"
24
- PHONETIC = "phonetic"
25
- HEADER = "header"
26
- FOOTER = "footer"
27
- PAGE_NUMBER = "page_number"
28
- ASIDE_TEXT = "aside_text"
29
- PAGE_FOOTNOTE = "page_footnote"
30
- SEAL = 'seal'
31
- CHAR = 'char'
32
-
33
- class ContentType:
34
- IMAGE = 'image'
35
- TABLE = 'table'
36
- TEXT = 'text'
37
- INTERLINE_EQUATION = 'interline_equation'
38
- INLINE_EQUATION = 'inline_equation'
39
- EQUATION = 'equation'
40
- CODE = 'code'
41
- SEAL = 'seal'
42
- CHAR = 'char'
43
-
44
- class SplitFlag:
45
- CROSS_PAGE = 'cross_page'
46
- LINES_DELETED = 'lines_deleted'
47
-
48
-
49
- class ImageType:
50
- PIL = 'pil_img'
51
- BASE64 = 'base64_img'
52
-
53
-
54
- class NotExtractType(Enum):
55
- TEXT = BlockType.TEXT
56
- TITLE = BlockType.TITLE
57
- HEADER = BlockType.HEADER
58
- FOOTER = BlockType.FOOTER
59
- PAGE_NUMBER = BlockType.PAGE_NUMBER
60
- PAGE_FOOTNOTE = BlockType.PAGE_FOOTNOTE
61
- REF_TEXT = BlockType.REF_TEXT
62
- TABLE_CAPTION = BlockType.TABLE_CAPTION
63
- IMAGE_CAPTION = BlockType.IMAGE_CAPTION
64
- TABLE_FOOTNOTE = BlockType.TABLE_FOOTNOTE
65
- IMAGE_FOOTNOTE = BlockType.IMAGE_FOOTNOTE
66
- CODE_CAPTION = BlockType.CODE_CAPTION
67
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/guess_suffix_or_lang.py DELETED
@@ -1,55 +0,0 @@
1
- from pathlib import Path
2
-
3
- from loguru import logger
4
- from magika import Magika
5
-
6
-
7
- DEFAULT_LANG = "txt"
8
- PDF_SIG_BYTES = b'%PDF'
9
- magika = Magika()
10
-
11
- def code_content_clean(content):
12
- if not content:
13
- return ""
14
- lines = content.splitlines()
15
- start_idx = 0
16
- end_idx = len(lines)
17
- if lines and lines[0].startswith("```"):
18
- start_idx = 1
19
- if lines and end_idx > start_idx and lines[end_idx - 1].strip() == "```":
20
- end_idx -= 1
21
- if start_idx < end_idx:
22
- return "\n".join(lines[start_idx:end_idx]).strip()
23
- return ""
24
-
25
- def guess_language_by_text(code):
26
- code = code_content_clean(code)
27
- if code.startswith("<_"):
28
- end = code.find("_>")
29
- if end != -1:
30
- lang = code[2:end].lower()
31
- code = code[end + 2:]
32
- return lang, code
33
-
34
- return (lang if lang != "unknown" else DEFAULT_LANG), code
35
-
36
-
37
- def guess_suffix_by_bytes(file_bytes, file_path=None) -> str:
38
- suffix = magika.identify_bytes(file_bytes).prediction.output.label
39
- if file_path and suffix in ["ai", "html"] and Path(file_path).suffix.lower() in [".pdf"] and file_bytes[:4] == PDF_SIG_BYTES:
40
- suffix = "pdf"
41
- return suffix
42
-
43
-
44
- def guess_suffix_by_path(file_path) -> str:
45
- if not isinstance(file_path, Path):
46
- file_path = Path(file_path)
47
- suffix = magika.identify_path(file_path).prediction.output.label
48
- if suffix in ["ai", "html"] and file_path.suffix.lower() in [".pdf"]:
49
- try:
50
- with open(file_path, 'rb') as f:
51
- if f.read(4) == PDF_SIG_BYTES:
52
- suffix = "pdf"
53
- except Exception as e:
54
- logger.warning(f"Failed to read file {file_path} for PDF signature check: {e}")
55
- return suffix
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/hash_utils.py DELETED
@@ -1,30 +0,0 @@
1
-
2
- import hashlib
3
- import json
4
-
5
-
6
- def bytes_md5(file_bytes):
7
- hasher = hashlib.md5()
8
- hasher.update(file_bytes)
9
- return hasher.hexdigest().upper()
10
-
11
-
12
- def str_md5(input_string):
13
- hasher = hashlib.md5()
14
- # 在Python3中,需要将字符串转化为字节对象才能被哈希函数处理
15
- input_bytes = input_string.encode('utf-8')
16
- hasher.update(input_bytes)
17
- return hasher.hexdigest()
18
-
19
-
20
- def str_sha256(input_string):
21
- hasher = hashlib.sha256()
22
- # 在Python3中,需要将字符串转化为字节对象才能被哈希函数处理
23
- input_bytes = input_string.encode('utf-8')
24
- hasher.update(input_bytes)
25
- return hasher.hexdigest()
26
-
27
-
28
- def dict_md5(d):
29
- json_str = json.dumps(d, sort_keys=True, ensure_ascii=False)
30
- return hashlib.md5(json_str.encode('utf-8')).hexdigest()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/language.py DELETED
@@ -1,48 +0,0 @@
1
- import os
2
- import unicodedata
3
-
4
- if not os.getenv("FTLANG_CACHE"):
5
- current_file_path = os.path.abspath(__file__)
6
- current_dir = os.path.dirname(current_file_path)
7
- root_dir = os.path.dirname(current_dir)
8
- ftlang_cache_dir = os.path.join(root_dir, 'resources', 'fasttext-langdetect')
9
- os.environ["FTLANG_CACHE"] = str(ftlang_cache_dir)
10
- # print(os.getenv("FTLANG_CACHE"))
11
-
12
- from fast_langdetect import detect_language
13
-
14
-
15
- def remove_invalid_surrogates(text):
16
- # 移除无效的 UTF-16 代理对
17
- return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
18
-
19
-
20
- def detect_lang(text: str, MAX_LANG_LEN = 128) -> str:
21
-
22
- if not text:
23
- return ""
24
-
25
- text = text.replace("\n", "")
26
- text = remove_invalid_surrogates(text)
27
-
28
- text = text[:MAX_LANG_LEN]
29
- try:
30
- lang_upper = detect_language(text)
31
- except:
32
- html_no_ctrl_chars = ''.join([l for l in text if unicodedata.category(l)[0] not in ['C', ]])
33
- lang_upper = detect_language(html_no_ctrl_chars)
34
-
35
- try:
36
- lang = lang_upper.lower()
37
- except:
38
- lang = ""
39
- return lang
40
-
41
-
42
- if __name__ == '__main__':
43
- print(os.getenv("FTLANG_CACHE"))
44
- print(detect_lang("This is a test."*100000))
45
- print(detect_lang("<html>This is a test</html>"))
46
- print(detect_lang("这个是中文测试。"))
47
- print(detect_lang("<html>这个是中文测试。</html>"))
48
- print(detect_lang("〖\ud835\udc46\ud835〗这是个包含utf-16的中文测试"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/magic_model_utils.py DELETED
@@ -1,251 +0,0 @@
1
- """
2
- 包含两个MagicModel类中重复使用的方法和逻辑
3
- """
4
- from typing import List, Dict, Any, Callable
5
- from NaviOCR.tools.boxbase import bbox_distance, bbox_center_distance, is_in
6
-
7
-
8
- def reduct_overlap(bboxes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
9
- """
10
- 去除重叠的bbox,保留不被其他bbox包含的bbox
11
-
12
- Args:
13
- bboxes: 包含bbox信息的字典列表
14
-
15
- Returns:
16
- 去重后的bbox列表
17
- """
18
- N = len(bboxes)
19
- keep = [True] * N
20
- for i in range(N):
21
- for j in range(N):
22
- if i == j:
23
- continue
24
- if is_in(bboxes[i]['bbox'], bboxes[j]['bbox']):
25
- keep[i] = False
26
- return [bboxes[i] for i in range(N) if keep[i]]
27
-
28
-
29
- def tie_up_category_by_distance_v3(
30
- get_subjects_func: Callable,
31
- get_objects_func: Callable,
32
- extract_subject_func: Callable = None,
33
- extract_object_func: Callable = None
34
- ):
35
- """
36
- 通用的类别关联方法,用于将主体对象与客体对象进行关联
37
-
38
- 参数:
39
- get_subjects_func: 函数,提取主体对象
40
- get_objects_func: 函数,提取客体对象
41
- extract_subject_func: 函数,自定义提取主体属性(默认使用bbox和其他属性)
42
- extract_object_func: 函数,自定义提取客体属性(默认使用bbox和其他属性)
43
-
44
- 返回:
45
- 关联后的对象列表
46
- """
47
- subjects = get_subjects_func()
48
- objects = get_objects_func()
49
-
50
- # 如果没有提供自定义提取函数,使用默认函数
51
- if extract_subject_func is None:
52
- extract_subject_func = lambda x: x
53
- if extract_object_func is None:
54
- extract_object_func = lambda x: x
55
-
56
- ret = []
57
- N, M = len(subjects), len(objects)
58
- subjects.sort(key=lambda x: x["bbox"][0] ** 2 + x["bbox"][1] ** 2)
59
- objects.sort(key=lambda x: x["bbox"][0] ** 2 + x["bbox"][1] ** 2)
60
-
61
- OBJ_IDX_OFFSET = 10000
62
- SUB_BIT_KIND, OBJ_BIT_KIND = 0, 1
63
-
64
- all_boxes_with_idx = [(i, SUB_BIT_KIND, sub["bbox"][0], sub["bbox"][1]) for i, sub in enumerate(subjects)] + [
65
- (i + OBJ_IDX_OFFSET, OBJ_BIT_KIND, obj["bbox"][0], obj["bbox"][1]) for i, obj in enumerate(objects)
66
- ]
67
- seen_idx = set()
68
- seen_sub_idx = set()
69
-
70
- while N > len(seen_sub_idx):
71
- candidates = []
72
- for idx, kind, x0, y0 in all_boxes_with_idx:
73
- if idx in seen_idx:
74
- continue
75
- candidates.append((idx, kind, x0, y0))
76
-
77
- if len(candidates) == 0:
78
- break
79
- left_x = min([v[2] for v in candidates])
80
- top_y = min([v[3] for v in candidates])
81
-
82
- candidates.sort(key=lambda x: (x[2] - left_x) ** 2 + (x[3] - top_y) ** 2)
83
-
84
- fst_idx, fst_kind, left_x, top_y = candidates[0]
85
- fst_bbox = subjects[fst_idx]['bbox'] if fst_kind == SUB_BIT_KIND else objects[fst_idx - OBJ_IDX_OFFSET]['bbox']
86
- candidates.sort(
87
- key=lambda x: bbox_distance(fst_bbox, subjects[x[0]]['bbox']) if x[1] == SUB_BIT_KIND else bbox_distance(
88
- fst_bbox, objects[x[0] - OBJ_IDX_OFFSET]['bbox']))
89
- nxt = None
90
-
91
- for i in range(1, len(candidates)):
92
- if candidates[i][1] ^ fst_kind == 1:
93
- nxt = candidates[i]
94
- break
95
- if nxt is None:
96
- break
97
-
98
- if fst_kind == SUB_BIT_KIND:
99
- sub_idx, obj_idx = fst_idx, nxt[0] - OBJ_IDX_OFFSET
100
- else:
101
- sub_idx, obj_idx = nxt[0], fst_idx - OBJ_IDX_OFFSET
102
-
103
- pair_dis = bbox_distance(subjects[sub_idx]["bbox"], objects[obj_idx]["bbox"])
104
- nearest_dis = float("inf")
105
- for i in range(N):
106
- # 取消原先算法中 1对1 匹配的偏置
107
- # if i in seen_idx or i == sub_idx:continue
108
- nearest_dis = min(nearest_dis, bbox_distance(subjects[i]["bbox"], objects[obj_idx]["bbox"]))
109
-
110
- if pair_dis >= 3 * nearest_dis:
111
- seen_idx.add(sub_idx)
112
- continue
113
-
114
- seen_idx.add(sub_idx)
115
- seen_idx.add(obj_idx + OBJ_IDX_OFFSET)
116
- seen_sub_idx.add(sub_idx)
117
-
118
- ret.append(
119
- {
120
- "sub_bbox": extract_subject_func(subjects[sub_idx]),
121
- "obj_bboxes": [extract_object_func(objects[obj_idx])],
122
- "sub_idx": sub_idx,
123
- }
124
- )
125
-
126
- for i in range(len(objects)):
127
- j = i + OBJ_IDX_OFFSET
128
- if j in seen_idx:
129
- continue
130
- seen_idx.add(j)
131
- nearest_dis, nearest_sub_idx = float("inf"), -1
132
- for k in range(len(subjects)):
133
- dis = bbox_distance(objects[i]["bbox"], subjects[k]["bbox"])
134
- if dis < nearest_dis:
135
- nearest_dis = dis
136
- nearest_sub_idx = k
137
-
138
- for k in range(len(subjects)):
139
- if k != nearest_sub_idx:
140
- continue
141
- if k in seen_sub_idx:
142
- for kk in range(len(ret)):
143
- if ret[kk]["sub_idx"] == k:
144
- ret[kk]["obj_bboxes"].append(extract_object_func(objects[i]))
145
- break
146
- else:
147
- ret.append(
148
- {
149
- "sub_bbox": extract_subject_func(subjects[k]),
150
- "obj_bboxes": [extract_object_func(objects[i])],
151
- "sub_idx": k,
152
- }
153
- )
154
- seen_sub_idx.add(k)
155
- seen_idx.add(k)
156
-
157
- for i in range(len(subjects)):
158
- if i in seen_sub_idx:
159
- continue
160
- ret.append(
161
- {
162
- "sub_bbox": extract_subject_func(subjects[i]),
163
- "obj_bboxes": [],
164
- "sub_idx": i,
165
- }
166
- )
167
-
168
- return ret
169
-
170
-
171
- def tie_up_category_by_index(
172
- get_subjects_func: Callable,
173
- get_objects_func: Callable,
174
- extract_subject_func: Callable = None,
175
- extract_object_func: Callable = None
176
- ):
177
- """
178
- 基于index的类别关联方法,用于将主体对象与客体对象进行关联
179
- 客体优先匹配给index最接近的主体,index差值相同时使用bbox中心点距离作为tiebreaker
180
-
181
- 参数:
182
- get_subjects_func: 函数,提取主体对象
183
- get_objects_func: 函数,提取客体对象
184
- extract_subject_func: 函数,自定义提取主体属性(默认使用bbox和其他属性)
185
- extract_object_func: 函数,自定义提取客体属性(默认使用bbox和其他属性)
186
-
187
- 返回:
188
- 关联后的对象列表,按主体index升序排列
189
- """
190
- subjects = get_subjects_func()
191
- objects = get_objects_func()
192
-
193
- # 如果没有提供自定义提取函数,使用默认函数
194
- if extract_subject_func is None:
195
- extract_subject_func = lambda x: x
196
- if extract_object_func is None:
197
- extract_object_func = lambda x: x
198
-
199
- # 初始化结果字典,key为主体索引,value为关联信息
200
- result_dict = {}
201
-
202
- # 初始化所有主体
203
- for i, subject in enumerate(subjects):
204
- result_dict[i] = {
205
- "sub_bbox": extract_subject_func(subject),
206
- "obj_bboxes": [],
207
- "sub_idx": i,
208
- }
209
-
210
- # 为每个客体找到最匹配的主体
211
- for obj in objects:
212
- if len(subjects) == 0:
213
- # 如果没有主体,跳过客体
214
- continue
215
-
216
- obj_index = obj["index"]
217
- min_index_diff = float("inf")
218
- best_subject_indices = []
219
-
220
- # 找出index差值最小的所有主体
221
- for i, subject in enumerate(subjects):
222
- sub_index = subject["index"]
223
- index_diff = abs(obj_index - sub_index)
224
-
225
- if index_diff < min_index_diff:
226
- min_index_diff = index_diff
227
- best_subject_indices = [i]
228
- elif index_diff == min_index_diff:
229
- best_subject_indices.append(i)
230
-
231
- # 如果有多个主体的index差值相同,使用中心点距离作为tiebreaker
232
- if len(best_subject_indices) > 1:
233
- min_center_dist = float("inf")
234
- best_subject_idx = best_subject_indices[0]
235
-
236
- for idx in best_subject_indices:
237
- center_dist = bbox_center_distance(obj["bbox"], subjects[idx]["bbox"])
238
- if center_dist < min_center_dist:
239
- min_center_dist = center_dist
240
- best_subject_idx = idx
241
- else:
242
- best_subject_idx = best_subject_indices[0]
243
-
244
- # 将客体添加到最佳主体的obj_bboxes中
245
- result_dict[best_subject_idx]["obj_bboxes"].append(extract_object_func(obj))
246
-
247
- # 转换为列表并按主体index排序
248
- ret = list(result_dict.values())
249
- ret.sort(key=lambda x: x["sub_idx"])
250
-
251
- return ret
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/model_utils.py DELETED
@@ -1,462 +0,0 @@
1
- import os
2
- import time
3
- import gc
4
- from PIL import Image
5
- from loguru import logger
6
- import numpy as np
7
-
8
- from NaviOCR.tools.boxbase import get_minbox_if_overlap_by_ratio
9
-
10
- try:
11
- import torch
12
- import torch_npu
13
- except ImportError:
14
- pass
15
-
16
-
17
- def crop_img(input_res, input_img, crop_paste_x=0, crop_paste_y=0):
18
-
19
- crop_xmin, crop_ymin = int(input_res['poly'][0]), int(input_res['poly'][1])
20
- crop_xmax, crop_ymax = int(input_res['poly'][4]), int(input_res['poly'][5])
21
-
22
- # Calculate new dimensions
23
- crop_new_width = crop_xmax - crop_xmin + crop_paste_x * 2
24
- crop_new_height = crop_ymax - crop_ymin + crop_paste_y * 2
25
-
26
- if isinstance(input_img, np.ndarray):
27
-
28
- # Create a white background array
29
- return_image = np.ones((crop_new_height, crop_new_width, 3), dtype=np.uint8) * 255
30
-
31
- # Crop the original image using numpy slicing
32
- cropped_img = input_img[crop_ymin:crop_ymax, crop_xmin:crop_xmax]
33
-
34
- # Paste the cropped image onto the white background
35
- return_image[crop_paste_y:crop_paste_y + (crop_ymax - crop_ymin),
36
- crop_paste_x:crop_paste_x + (crop_xmax - crop_xmin)] = cropped_img
37
- else:
38
- # Create a white background array
39
- return_image = Image.new('RGB', (crop_new_width, crop_new_height), 'white')
40
- # Crop image
41
- crop_box = (crop_xmin, crop_ymin, crop_xmax, crop_ymax)
42
- cropped_img = input_img.crop(crop_box)
43
- return_image.paste(cropped_img, (crop_paste_x, crop_paste_y))
44
-
45
- return_list = [crop_paste_x, crop_paste_y, crop_xmin, crop_ymin, crop_xmax, crop_ymax, crop_new_width,
46
- crop_new_height]
47
- return return_image, return_list
48
-
49
-
50
- def get_coords_and_area(block_with_poly):
51
- """Extract coordinates and area from a table."""
52
- xmin, ymin = int(block_with_poly['poly'][0]), int(block_with_poly['poly'][1])
53
- xmax, ymax = int(block_with_poly['poly'][4]), int(block_with_poly['poly'][5])
54
- area = (xmax - xmin) * (ymax - ymin)
55
- return xmin, ymin, xmax, ymax, area
56
-
57
-
58
- def calculate_intersection(box1, box2):
59
- """Calculate intersection coordinates between two boxes."""
60
- intersection_xmin = max(box1[0], box2[0])
61
- intersection_ymin = max(box1[1], box2[1])
62
- intersection_xmax = min(box1[2], box2[2])
63
- intersection_ymax = min(box1[3], box2[3])
64
-
65
- # Check if intersection is valid
66
- if intersection_xmax <= intersection_xmin or intersection_ymax <= intersection_ymin:
67
- return None
68
-
69
- return intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax
70
-
71
-
72
- def calculate_iou(box1, box2):
73
- """Calculate IoU between two boxes."""
74
- intersection = calculate_intersection(box1[:4], box2[:4])
75
-
76
- if not intersection:
77
- return 0
78
-
79
- intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax = intersection
80
- intersection_area = (intersection_xmax - intersection_xmin) * (intersection_ymax - intersection_ymin)
81
-
82
- area1, area2 = box1[4], box2[4]
83
- union_area = area1 + area2 - intersection_area
84
-
85
- return intersection_area / union_area if union_area > 0 else 0
86
-
87
-
88
- def is_inside(small_box, big_box, overlap_threshold=0.8):
89
- """Check if small_box is inside big_box by at least overlap_threshold."""
90
- intersection = calculate_intersection(small_box[:4], big_box[:4])
91
-
92
- if not intersection:
93
- return False
94
-
95
- intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax = intersection
96
- intersection_area = (intersection_xmax - intersection_xmin) * (intersection_ymax - intersection_ymin)
97
-
98
- # Check if overlap exceeds threshold
99
- return intersection_area >= overlap_threshold * small_box[4]
100
-
101
-
102
- def do_overlap(box1, box2):
103
- """Check if two boxes overlap."""
104
- return calculate_intersection(box1[:4], box2[:4]) is not None
105
-
106
-
107
- def merge_high_iou_tables(table_res_list, layout_res, table_indices, iou_threshold=0.7):
108
- """Merge tables with IoU > threshold."""
109
- if len(table_res_list) < 2:
110
- return table_res_list, table_indices
111
-
112
- table_info = [get_coords_and_area(table) for table in table_res_list]
113
- merged = True
114
-
115
- while merged:
116
- merged = False
117
- i = 0
118
- while i < len(table_res_list) - 1:
119
- j = i + 1
120
- while j < len(table_res_list):
121
- iou = calculate_iou(table_info[i], table_info[j])
122
-
123
- if iou > iou_threshold:
124
- # Merge tables by taking their union
125
- x1_min, y1_min, x1_max, y1_max, _ = table_info[i]
126
- x2_min, y2_min, x2_max, y2_max, _ = table_info[j]
127
-
128
- union_xmin = min(x1_min, x2_min)
129
- union_ymin = min(y1_min, y2_min)
130
- union_xmax = max(x1_max, x2_max)
131
- union_ymax = max(y1_max, y2_max)
132
-
133
- # Create merged table
134
- merged_table = table_res_list[i].copy()
135
- merged_table['poly'] = [
136
- union_xmin, union_ymin, union_xmax, union_ymin,
137
- union_xmax, union_ymax, union_xmin, union_ymax
138
- ]
139
- # Update layout_res
140
- to_remove = [table_indices[j], table_indices[i]]
141
- for idx in sorted(to_remove, reverse=True):
142
- del layout_res[idx]
143
- layout_res.append(merged_table)
144
-
145
- # Update tracking lists
146
- table_indices = [k if k < min(to_remove) else
147
- k - 1 if k < max(to_remove) else
148
- k - 2 if k > max(to_remove) else
149
- len(layout_res) - 1
150
- for k in table_indices
151
- if k not in to_remove]
152
- table_indices.append(len(layout_res) - 1)
153
-
154
- # Update table lists
155
- table_res_list.pop(j)
156
- table_res_list.pop(i)
157
- table_res_list.append(merged_table)
158
-
159
- # Update table_info
160
- table_info = [get_coords_and_area(table) for table in table_res_list]
161
-
162
- merged = True
163
- break
164
- j += 1
165
-
166
- if merged:
167
- break
168
- i += 1
169
-
170
- return table_res_list, table_indices
171
-
172
-
173
- def filter_nested_tables(table_res_list, overlap_threshold=0.8, area_threshold=0.8):
174
- """Remove big tables containing multiple smaller tables within them."""
175
- if len(table_res_list) < 3:
176
- return table_res_list
177
-
178
- table_info = [get_coords_and_area(table) for table in table_res_list]
179
- big_tables_idx = []
180
-
181
- for i in range(len(table_res_list)):
182
- # Find tables inside this one
183
- tables_inside = [j for j in range(len(table_res_list))
184
- if i != j and is_inside(table_info[j], table_info[i], overlap_threshold)]
185
-
186
- # Continue if there are at least 3 tables inside
187
- if len(tables_inside) >= 3:
188
- # Check if inside tables overlap with each other
189
- tables_overlap = any(do_overlap(table_info[tables_inside[idx1]], table_info[tables_inside[idx2]])
190
- for idx1 in range(len(tables_inside))
191
- for idx2 in range(idx1 + 1, len(tables_inside)))
192
-
193
- # If no overlaps, check area condition
194
- if not tables_overlap:
195
- total_inside_area = sum(table_info[j][4] for j in tables_inside)
196
- big_table_area = table_info[i][4]
197
-
198
- if total_inside_area > area_threshold * big_table_area:
199
- big_tables_idx.append(i)
200
-
201
- return [table for i, table in enumerate(table_res_list) if i not in big_tables_idx]
202
-
203
-
204
- def remove_overlaps_min_blocks(res_list):
205
-
206
- for res in res_list:
207
- res['bbox'] = [int(res['poly'][0]), int(res['poly'][1]), int(res['poly'][4]), int(res['poly'][5])]
208
-
209
- # 重叠block,小的不能直接删除,需要和大的那个合并成一个更大的。
210
- # 删除重叠blocks中较小的那些
211
- need_remove = []
212
- for i in range(len(res_list)):
213
- # 如果当前元素已在需要移除列表中,则跳过
214
- if res_list[i] in need_remove:
215
- continue
216
-
217
- for j in range(i + 1, len(res_list)):
218
- # 如果比较对象已在需要移除列表中,则跳过
219
- if res_list[j] in need_remove:
220
- continue
221
-
222
- overlap_box = get_minbox_if_overlap_by_ratio(
223
- res_list[i]['bbox'], res_list[j]['bbox'], 0.8
224
- )
225
-
226
- if overlap_box is not None:
227
-
228
- # 根据重叠框确定哪个是小块,哪个是大块
229
- if overlap_box == res_list[i]['bbox']:
230
- small_res, large_res = res_list[i], res_list[j]
231
- elif overlap_box == res_list[j]['bbox']:
232
- small_res, large_res = res_list[j], res_list[i]
233
- else:
234
- continue # 如果重叠框与任一块都不匹配,跳过处理
235
-
236
- if small_res['score'] <= large_res['score']:
237
- # 如果小块的分数低于大块,则小块为需要移除的块
238
- if small_res is not None and small_res not in need_remove:
239
- # 更新大块的边界为两者的并集
240
- x1, y1, x2, y2 = large_res['bbox']
241
- sx1, sy1, sx2, sy2 = small_res['bbox']
242
- x1 = min(x1, sx1)
243
- y1 = min(y1, sy1)
244
- x2 = max(x2, sx2)
245
- y2 = max(y2, sy2)
246
- large_res['bbox'] = [x1, y1, x2, y2]
247
- need_remove.append(small_res)
248
- else:
249
- # 如果大块的分数低于小块,则大块为需要移除的块, 这时不需要更新小块的边界
250
- if large_res is not None and large_res not in need_remove:
251
- need_remove.append(large_res)
252
-
253
- # 从列表中移除标记的元素
254
- for res in need_remove:
255
- res_list.remove(res)
256
- del res['bbox'] # 删除bbox字段
257
-
258
- for res in res_list:
259
- # 将res的poly使用bbox重构
260
- res['poly'] = [res['bbox'][0], res['bbox'][1], res['bbox'][2], res['bbox'][1],
261
- res['bbox'][2], res['bbox'][3], res['bbox'][0], res['bbox'][3]]
262
- # 删除res的bbox
263
- del res['bbox']
264
-
265
- return res_list, need_remove
266
-
267
-
268
- def remove_overlaps_low_confidence_blocks(combined_res_list, overlap_threshold=0.8):
269
- """
270
- Remove low-confidence blocks that overlap with other blocks.
271
-
272
- This function identifies and removes blocks with low confidence scores that overlap
273
- with other blocks. It calculates the coordinates and area of each block, and checks
274
- for overlaps based on a specified threshold. Blocks that meet the criteria for removal
275
- are returned in a list.
276
-
277
- Parameters:
278
- combined_res_list (list): A list of blocks, where each block is a dictionary containing
279
- keys like 'poly' (polygon coordinates) and optionally 'score' (confidence score).
280
- overlap_threshold (float): The threshold for determining overlap between blocks. Default is 0.8.
281
-
282
- Returns:
283
- list: A list of blocks to be removed, based on the overlap and confidence criteria.
284
- """
285
- # 计算每个block的坐标和面积
286
- block_info = []
287
- for block in combined_res_list:
288
- xmin, ymin = int(block['poly'][0]), int(block['poly'][1])
289
- xmax, ymax = int(block['poly'][4]), int(block['poly'][5])
290
- area = (xmax - xmin) * (ymax - ymin)
291
- score = block.get('score', 0.5) # 如果没有score字段,默认为0.5
292
- block_info.append((xmin, ymin, xmax, ymax, area, score, block))
293
-
294
- blocks_to_remove = []
295
- marked_indices = set() # 跟踪已标记为删除的block索引
296
-
297
- # 检查每个block内部是否有3个及以上的小block
298
- for i, (xmin, ymin, xmax, ymax, area, score, block) in enumerate(block_info):
299
- # 如果当前block已标记为删除,则跳过
300
- if i in marked_indices:
301
- continue
302
-
303
- # 查找内部的小block (仅考虑尚未被标记为删除的block)
304
- blocks_inside = [(j, j_score, j_block) for j, (xj_min, yj_min, xj_max, yj_max, j_area, j_score, j_block) in
305
- enumerate(block_info)
306
- if i != j and j not in marked_indices and is_inside(block_info[j], block_info[i],
307
- overlap_threshold)]
308
-
309
- # 如果内部有3个及以上的小block
310
- if len(blocks_inside) >= 2:
311
- # 计算小block的平均分数
312
- avg_score = sum(s for _, s, _ in blocks_inside) / len(blocks_inside)
313
-
314
- # 比较大block的分数和小block的平均分数
315
- if score > avg_score:
316
- # 保留大block,扩展其边界
317
- # 首先将所有小block标记为要删除
318
- for j, _, j_block in blocks_inside:
319
- if j_block not in blocks_to_remove:
320
- blocks_to_remove.append(j_block)
321
- marked_indices.add(j) # 标记索引为已处理
322
-
323
- # 扩展大block的边界以包含所有小block
324
- new_xmin, new_ymin, new_xmax, new_ymax = xmin, ymin, xmax, ymax
325
- for _, _, j_block in blocks_inside:
326
- j_xmin, j_ymin = int(j_block['poly'][0]), int(j_block['poly'][1])
327
- j_xmax, j_ymax = int(j_block['poly'][4]), int(j_block['poly'][5])
328
- new_xmin = min(new_xmin, j_xmin)
329
- new_ymin = min(new_ymin, j_ymin)
330
- new_xmax = max(new_xmax, j_xmax)
331
- new_ymax = max(new_ymax, j_ymax)
332
-
333
- # 更新大block的边界
334
- block['poly'][0] = block['poly'][6] = new_xmin
335
- block['poly'][1] = block['poly'][3] = new_ymin
336
- block['poly'][2] = block['poly'][4] = new_xmax
337
- block['poly'][5] = block['poly'][7] = new_ymax
338
- else:
339
- # 保留小blocks,删除大block
340
- blocks_to_remove.append(block)
341
- marked_indices.add(i) # 标记当前索引为已处理
342
- return blocks_to_remove
343
-
344
-
345
- def get_res_list_from_layout_res(layout_res, iou_threshold=0.7, overlap_threshold=0.8, area_threshold=0.8):
346
- """Extract OCR, table and other regions from layout results."""
347
- ocr_res_list = []
348
- text_res_list = []
349
- table_res_list = []
350
- table_indices = []
351
- single_page_mfdetrec_res = []
352
-
353
- # Categorize regions
354
- for i, res in enumerate(layout_res):
355
- category_id = int(res['category_id'])
356
-
357
- if category_id in [13, 14]: # Formula regions
358
- single_page_mfdetrec_res.append({
359
- "bbox": [int(res['poly'][0]), int(res['poly'][1]),
360
- int(res['poly'][4]), int(res['poly'][5])],
361
- })
362
- elif category_id in [0, 2, 4, 6, 7, 3]: # OCR regions
363
- ocr_res_list.append(res)
364
- elif category_id == 5: # Table regions
365
- table_res_list.append(res)
366
- table_indices.append(i)
367
- elif category_id in [1]: # Text regions
368
- text_res_list.append(res)
369
-
370
- # Process tables: merge high IoU tables first, then filter nested tables
371
- table_res_list, table_indices = merge_high_iou_tables(
372
- table_res_list, layout_res, table_indices, iou_threshold)
373
-
374
- filtered_table_res_list = filter_nested_tables(
375
- table_res_list, overlap_threshold, area_threshold)
376
-
377
- filtered_table_res_list, table_need_remove = remove_overlaps_min_blocks(filtered_table_res_list)
378
-
379
- for res in table_need_remove:
380
- if res in layout_res:
381
- layout_res.remove(res)
382
-
383
- # Remove filtered out tables from layout_res
384
- if len(filtered_table_res_list) < len(table_res_list):
385
- kept_tables = set(id(table) for table in filtered_table_res_list)
386
- tables_to_remove = [table for table in table_res_list if id(table) not in kept_tables]
387
- for table in tables_to_remove:
388
- if table in layout_res:
389
- layout_res.remove(table)
390
-
391
- # Remove overlaps in OCR and text regions
392
- text_res_list, need_remove = remove_overlaps_min_blocks(text_res_list)
393
-
394
- ocr_res_list.extend(text_res_list)
395
-
396
- for res in need_remove:
397
- if res in layout_res:
398
- layout_res.remove(res)
399
-
400
- # 检测大block内部是否包含多个小block, 合并ocr和table列表进行检测
401
- combined_res_list = ocr_res_list + filtered_table_res_list
402
- blocks_to_remove = remove_overlaps_low_confidence_blocks(combined_res_list, overlap_threshold)
403
- # 移除需要删除的blocks
404
- for block in blocks_to_remove:
405
- if block in ocr_res_list:
406
- ocr_res_list.remove(block)
407
- elif block in filtered_table_res_list:
408
- filtered_table_res_list.remove(block)
409
- # 同时从layout_res中删除
410
- if block in layout_res:
411
- layout_res.remove(block)
412
-
413
- return ocr_res_list, filtered_table_res_list, single_page_mfdetrec_res
414
-
415
-
416
- def clean_memory(device='cuda'):
417
- if device == 'cuda':
418
- if torch.cuda.is_available():
419
- torch.cuda.empty_cache()
420
- torch.cuda.ipc_collect()
421
- elif str(device).startswith("npu"):
422
- if torch_npu.npu.is_available():
423
- torch_npu.npu.empty_cache()
424
- elif str(device).startswith("mps"):
425
- torch.mps.empty_cache()
426
- gc.collect()
427
-
428
-
429
- def clean_vram(device, vram_threshold=8):
430
- total_memory = get_vram(device)
431
- if total_memory and total_memory <= vram_threshold:
432
- gc_start = time.time()
433
- clean_memory(device)
434
- gc_time = round(time.time() - gc_start, 2)
435
- # logger.info(f"gc time: {gc_time}")
436
-
437
-
438
- def get_vram(device) -> int:
439
- env_vram = os.getenv("NaviOCR_VIRTUAL_VRAM_SIZE")
440
-
441
- # 如果环境变量已配置,尝试解析并返回
442
- if env_vram is not None:
443
- try:
444
- total_memory = int(env_vram)
445
- if total_memory > 0:
446
- return total_memory
447
- else:
448
- logger.warning(
449
- f"NaviOCR_VIRTUAL_VRAM_SIZE value '{env_vram}' is not positive, falling back to auto-detection")
450
- except ValueError:
451
- logger.warning(
452
- f"NaviOCR_VIRTUAL_VRAM_SIZE value '{env_vram}' is not a valid integer, falling back to auto-detection")
453
-
454
- # 环境变量未配置或配置错误,根据device自动获取
455
- total_memory = 1
456
- if torch.cuda.is_available() and str(device).startswith("cuda"):
457
- total_memory = round(torch.cuda.get_device_properties(device).total_memory / (1024 ** 3)) # 将字节转换为 GB
458
- elif str(device).startswith("npu"):
459
- if torch_npu.npu.is_available():
460
- total_memory = round(torch_npu.npu.get_device_properties(device).total_memory / (1024 ** 3)) # 转为 GB
461
-
462
- return total_memory
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/os_env_config.py DELETED
@@ -1,30 +0,0 @@
1
- import os
2
-
3
-
4
- def get_op_num_threads(env_name: str) -> int:
5
- env_value = os.getenv(env_name, None)
6
- return get_value_from_string(env_value, -1)
7
-
8
-
9
- def get_load_images_timeout() -> int:
10
- env_value = os.getenv('NaviOCR_PDF_RENDER_TIMEOUT', None)
11
- return get_value_from_string(env_value, 300)
12
-
13
-
14
- def get_value_from_string(env_value: str, default_value: int) -> int:
15
- if env_value is not None:
16
- try:
17
- num_threads = int(env_value)
18
- if num_threads > 0:
19
- return num_threads
20
- except ValueError:
21
- return default_value
22
- return default_value
23
-
24
-
25
- if __name__ == '__main__':
26
- print(get_value_from_string('1', -1))
27
- print(get_value_from_string('0', -1))
28
- print(get_value_from_string('-1', -1))
29
- print(get_value_from_string('abc', -1))
30
- print(get_load_images_timeout())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/pdf_image_tools.py DELETED
@@ -1,26 +0,0 @@
1
- import importlib
2
-
3
- import NaviOCR.config as CONFIG
4
-
5
- PDF_MODULES = {
6
- "PyMuPDF": ".pdf_image_tools_PyMuPDF",
7
- "pypdfium2": ".pdf_image_tools_pdfium",
8
- }
9
-
10
- module = importlib.import_module(
11
- PDF_MODULES[CONFIG.PDF_TOOLS],
12
- package=__package__,
13
- )
14
-
15
- for name in (
16
- "pdf_page_to_image",
17
- "_load_images_from_pdf_worker",
18
- "load_images_from_pdf",
19
- "load_images_from_pdf_core",
20
- "cut_image",
21
- "get_crop_img",
22
- "images_bytes_to_pdf_bytes",
23
- "get_page_size",
24
- "convert_pdf_bytes_to_bytes"
25
- ):
26
- globals()[name] = getattr(module, name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/pdf_image_tools_PyMuPDF.py DELETED
@@ -1,289 +0,0 @@
1
-
2
- import os
3
- from io import BytesIO
4
- from concurrent.futures import ProcessPoolExecutor, TimeoutError as FuturesTimeoutError
5
-
6
- import fitz # PyMuPDF
7
- import numpy as np
8
- from loguru import logger
9
- from PIL import Image, ImageOps
10
-
11
- import NaviOCR.config as CONFIG
12
- from NaviOCR.tools.check_sys_env import is_windows_environment
13
- from NaviOCR.tools.enum_class import ImageType
14
- from NaviOCR.tools.hash_utils import str_sha256
15
- from NaviOCR.tools.os_env_config import get_load_images_timeout
16
- from NaviOCR.tools.pdf_page_id import get_end_page_id
17
- from NaviOCR.tools.pdf_reader import image_to_b64str, image_to_bytes
18
-
19
- def convert_pdf_bytes_to_bytes(pdf_bytes,valid_single_page_ids=None):
20
- pdf = fitz.open(stream=pdf_bytes, filetype="pdf")
21
- output_pdf = fitz.open()
22
- try:
23
- total_pages = len(pdf)
24
-
25
- if not valid_single_page_ids:
26
- valid_single_page_ids = list(range(total_pages))
27
-
28
- valid_single_page_ids = [
29
- page_id
30
- for page_id in valid_single_page_ids
31
- if 0 <= page_id < total_pages
32
- ]
33
-
34
- for page_index in valid_single_page_ids:
35
- try:
36
- output_pdf.insert_pdf(
37
- pdf,
38
- from_page=page_index,
39
- to_page=page_index,
40
- )
41
- except Exception as page_error:
42
- logger.warning(
43
- f"Failed to import page {page_index}: "
44
- f"{page_error}, skipping this page."
45
- )
46
-
47
- output_bytes = output_pdf.tobytes(
48
- garbage=4,
49
- deflate=True,
50
- )
51
-
52
- except Exception as e:
53
- logger.warning(
54
- f"Error in converting PDF bytes: {e}, "
55
- f"Using original PDF bytes."
56
- )
57
- output_bytes = pdf_bytes
58
-
59
- finally:
60
- pdf.close()
61
- output_pdf.close()
62
-
63
- return output_bytes
64
-
65
- def pdf_page_to_image(page: fitz.Page,dpi=200,image_type=ImageType.PIL) -> dict:
66
- zoom = dpi / 72.0
67
- matrix = fitz.Matrix(zoom, zoom)
68
- pix = page.get_pixmap(matrix=matrix, alpha=False)
69
- pil_img = Image.frombytes("RGB",(pix.width, pix.height),pix.samples,)
70
-
71
- image_dict = {"scale": zoom,}
72
-
73
- if image_type == ImageType.BASE64:
74
- image_dict["img_base64"] = image_to_b64str(pil_img)
75
- else:
76
- image_dict["img_pil"] = pil_img
77
-
78
- return image_dict
79
-
80
-
81
- def _load_images_from_pdf_worker(
82
- pdf_bytes,
83
- dpi,
84
- start_page_id,
85
- end_page_id,
86
- image_type,
87
- ):
88
- """供多进程调用"""
89
- return load_images_from_pdf_core(
90
- pdf_bytes,
91
- dpi,
92
- start_page_id,
93
- end_page_id,
94
- image_type,
95
- )
96
-
97
-
98
- def load_images_from_pdf(
99
- pdf_bytes: bytes,
100
- dpi=200,
101
- start_page_id=0,
102
- end_page_id=None,
103
- image_type=ImageType.PIL,
104
- timeout=None,
105
- threads=4,
106
- ):
107
- """
108
- 带超时控制的 PDF 转图片函数,支持多进程。
109
-
110
- Args:
111
- pdf_bytes (bytes): PDF bytes.
112
- dpi (int): render dpi.
113
- start_page_id (int): start page.
114
- end_page_id (int | None): end page.
115
- image_type (ImageType): PIL or BASE64.
116
- timeout (int | None): timeout seconds.
117
- threads (int): process number.
118
- """
119
-
120
- pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
121
-
122
- if is_windows_environment() or CONFIG.PDF_TOOLS_WORKER_MAX_NUM==0:
123
- return (
124
- load_images_from_pdf_core(
125
- pdf_bytes,
126
- dpi,
127
- start_page_id,
128
- get_end_page_id(end_page_id, len(pdf_doc)),
129
- image_type,
130
- ),
131
- pdf_doc,
132
- )
133
-
134
- if timeout is None:
135
- timeout = get_load_images_timeout()
136
-
137
- end_page_id = get_end_page_id(end_page_id, len(pdf_doc))
138
-
139
- total_pages = end_page_id - start_page_id + 1
140
-
141
- actual_threads = min(max(1, int(os.cpu_count() * CONFIG.PDF_TOOLS_WORKER_RATIO)), threads, total_pages)
142
-
143
- pages_per_thread = max(1, total_pages // actual_threads)
144
-
145
- page_ranges = []
146
-
147
- for i in range(actual_threads):
148
- range_start = start_page_id + i * pages_per_thread
149
-
150
- if i == actual_threads - 1:
151
- range_end = end_page_id
152
- else:
153
- range_end = start_page_id + (i + 1) * pages_per_thread - 1
154
-
155
- page_ranges.append((range_start, range_end))
156
-
157
-
158
- with ProcessPoolExecutor(max_workers=actual_threads) as executor:
159
-
160
- futures = []
161
-
162
- for range_start, range_end in page_ranges:
163
- future = executor.submit(
164
- _load_images_from_pdf_worker,
165
- pdf_bytes,
166
- dpi,
167
- range_start,
168
- range_end,
169
- image_type,
170
- )
171
- futures.append((range_start, future))
172
-
173
- try:
174
- all_results = []
175
-
176
- for range_start, future in futures:
177
- images_list = future.result(timeout=timeout)
178
- all_results.append((range_start, images_list))
179
-
180
- all_results.sort(key=lambda x: x[0])
181
-
182
- images_list = []
183
-
184
- for _, imgs in all_results:
185
- images_list.extend(imgs)
186
-
187
- return images_list, pdf_doc
188
-
189
- except FuturesTimeoutError:
190
- pdf_doc.close()
191
- executor.shutdown(wait=False, cancel_futures=True)
192
- raise TimeoutError(
193
- f"PDF to images conversion timeout after {timeout}s"
194
- )
195
-
196
- def load_images_from_pdf_core(
197
- pdf_bytes: bytes,
198
- dpi=200,
199
- start_page_id=0,
200
- end_page_id=None,
201
- image_type=ImageType.PIL, # PIL or BASE64
202
- ):
203
- images_list = []
204
-
205
- pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
206
-
207
- pdf_page_num = len(pdf_doc)
208
-
209
- end_page_id = get_end_page_id(end_page_id, pdf_page_num)
210
-
211
- for index in range(start_page_id, end_page_id + 1):
212
- # logger.debug(f"Converting page {index}/{pdf_page_num} to image")
213
- page = pdf_doc[index]
214
- image_dict = pdf_page_to_image(
215
- page,
216
- dpi=dpi,
217
- image_type=image_type,
218
- )
219
- images_list.append(image_dict)
220
-
221
- pdf_doc.close()
222
-
223
- return images_list
224
-
225
- def cut_image(
226
- bbox: tuple,
227
- page_num: int,
228
- page_pil_img,
229
- return_path,
230
- image_writer,
231
- scale=2,
232
- ):
233
- filename = f"{page_num}_{int(bbox[0])}_{int(bbox[1])}_{int(bbox[2])}_{int(bbox[3])}"
234
- rel_img_path = f"{return_path}_{filename}.jpeg" if return_path is not None else None
235
- crop_img = get_crop_img(bbox, page_pil_img, scale=scale)
236
- img_bytes = image_to_bytes(crop_img, image_format="JPEG")
237
- image_writer.add_image(img_bytes, rel_img_path)
238
- return rel_img_path
239
-
240
-
241
- def get_crop_img(bbox: tuple, pil_img, scale=2):
242
- scale_bbox = (
243
- int(bbox[0] * scale),
244
- int(bbox[1] * scale),
245
- int(bbox[2] * scale),
246
- int(bbox[3] * scale),
247
- )
248
- return pil_img.crop(scale_bbox)
249
-
250
-
251
- # def get_crop_np_img(bbox: tuple, input_img, scale=2):
252
- # if isinstance(input_img, Image.Image):
253
- # np_img = np.asarray(input_img)
254
- # elif isinstance(input_img, np.ndarray):
255
- # np_img = input_img
256
- # else:
257
- # raise ValueError("Input must be a pillow object or a numpy array.")
258
-
259
- # scale_bbox = (
260
- # int(bbox[0] * scale),
261
- # int(bbox[1] * scale),
262
- # int(bbox[2] * scale),
263
- # int(bbox[3] * scale),
264
- # )
265
- # return np_img[scale_bbox[1] : scale_bbox[3], scale_bbox[0] : scale_bbox[2]]
266
-
267
-
268
- def images_bytes_to_pdf_bytes(image_bytes):
269
- pdf_buffer = BytesIO()
270
- image = Image.open(BytesIO(image_bytes))
271
- image = ImageOps.exif_transpose(image) or image
272
- if image.mode != "RGB":
273
- image = image.convert("RGB")
274
- image.save(
275
- pdf_buffer,
276
- format="PDF",
277
- # save_all=True
278
- )
279
- pdf_bytes = pdf_buffer.getvalue()
280
- pdf_buffer.close()
281
- return pdf_bytes
282
-
283
-
284
- def get_page_size(page):
285
- rect = page.rect
286
- w = rect.width
287
- h = rect.height
288
- return (w, h)
289
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/pdf_image_tools_pdfium.py DELETED
@@ -1,268 +0,0 @@
1
-
2
- import os
3
- from io import BytesIO
4
-
5
- import numpy as np
6
- import pypdfium2 as pdfium
7
- import io
8
- from loguru import logger
9
- from PIL import Image, ImageOps
10
-
11
- import NaviOCR.config as CONFIG
12
- from NaviOCR.tools.check_sys_env import is_windows_environment
13
- from NaviOCR.tools.os_env_config import get_load_images_timeout
14
- from NaviOCR.tools.pdf_reader import image_to_b64str, image_to_bytes, page_to_image
15
- from NaviOCR.tools.enum_class import ImageType
16
- from NaviOCR.tools.hash_utils import str_sha256
17
- from NaviOCR.tools.pdf_page_id import get_end_page_id
18
- from concurrent.futures import ProcessPoolExecutor, TimeoutError as FuturesTimeoutError
19
-
20
- def convert_pdf_bytes_to_bytes(pdf_bytes, valid_single_page_ids=None):
21
- pdf = pdfium.PdfDocument(pdf_bytes)
22
- output_pdf = pdfium.PdfDocument.new()
23
-
24
- try:
25
- total_pages = len(pdf)
26
- if not valid_single_page_ids:
27
- valid_single_page_ids = list(range(total_pages))
28
-
29
- valid_single_page_ids = [page_id for page_id in valid_single_page_ids if 0 <= page_id < total_pages]
30
-
31
- for page_index in valid_single_page_ids:
32
- try:
33
- output_pdf.import_pages(pdf, pages=[page_index])
34
- except Exception as page_error:
35
- logger.warning(
36
- f"Failed to import page {page_index}: "
37
- f"{page_error}, skipping this page."
38
- )
39
-
40
- output_buffer = io.BytesIO()
41
- output_pdf.save(output_buffer)
42
- output_bytes = output_buffer.getvalue()
43
-
44
- except Exception as e:
45
- logger.warning(
46
- f"Error in converting PDF bytes: {e}, "
47
- f"Using original PDF bytes."
48
- )
49
- output_bytes = pdf_bytes
50
-
51
- pdf.close()
52
- output_pdf.close()
53
-
54
- return output_bytes
55
-
56
- def pdf_page_to_image(page: pdfium.PdfPage, dpi=200, image_type=ImageType.PIL) -> dict:
57
- """Convert pdfium.PdfDocument to image, Then convert the image to base64.
58
-
59
- Args:
60
- page (_type_): pdfium.PdfPage
61
- dpi (int, optional): reset the dpi of dpi. Defaults to 200.
62
- image_type (ImageType, optional): The type of image to return. Defaults to ImageType.PIL.
63
-
64
- Returns:
65
- dict: {'img_base64': str, 'img_pil': pil_img, 'scale': float }
66
- """
67
- pil_img, scale = page_to_image(page, dpi=dpi)
68
- image_dict = {
69
- "scale": scale,
70
- }
71
- if image_type == ImageType.BASE64:
72
- image_dict["img_base64"] = image_to_b64str(pil_img)
73
- else:
74
- image_dict["img_pil"] = pil_img
75
-
76
- return image_dict
77
-
78
-
79
- def _load_images_from_pdf_worker(
80
- pdf_bytes, dpi, start_page_id, end_page_id, image_type
81
- ):
82
- """用于进程池的包装函数"""
83
- return load_images_from_pdf_core(
84
- pdf_bytes, dpi, start_page_id, end_page_id, image_type
85
- )
86
-
87
-
88
- def load_images_from_pdf(
89
- pdf_bytes: bytes,
90
- dpi=200,
91
- start_page_id=0,
92
- end_page_id=None,
93
- image_type=ImageType.PIL,
94
- timeout=None,
95
- threads=4,
96
- ):
97
- """带超时控制的 PDF 转图片函数,支持多进程加速
98
-
99
- Args:
100
- pdf_bytes (bytes): PDF 文件的 bytes
101
- dpi (int, optional): reset the dpi of dpi. Defaults to 200.
102
- start_page_id (int, optional): 起始页码. Defaults to 0.
103
- end_page_id (int | None, optional): 结束页码. Defaults to None.
104
- image_type (ImageType, optional): 图片类型. Defaults to ImageType.PIL.
105
- timeout (int | None, optional): 超时时间(秒)。如果为 None,则从环境变量 NaviOCR_PDF_LOAD_IMAGES_TIMEOUT 读取,若未设置则默认为 300 秒。
106
- threads (int): 进程数,默认 4
107
-
108
- Raises:
109
- TimeoutError: 当转换超时时抛出
110
- """
111
- pdf_doc = pdfium.PdfDocument(pdf_bytes)
112
- if is_windows_environment() or CONFIG.PDF_TOOLS_WORKER_MAX_NUM==0:
113
- # Windows 环境下不使用多进程
114
- return load_images_from_pdf_core(
115
- pdf_bytes,
116
- dpi,
117
- start_page_id,
118
- get_end_page_id(end_page_id, len(pdf_doc)),
119
- image_type,
120
- ), pdf_doc
121
- else:
122
- if timeout is None:
123
- timeout = get_load_images_timeout()
124
- end_page_id = get_end_page_id(end_page_id, len(pdf_doc))
125
-
126
- # 计算总页数
127
- total_pages = end_page_id - start_page_id + 1
128
-
129
- # 实际使用的进程数不超过总页数
130
- actual_threads = min(max(1, int(os.cpu_count() * CONFIG.PDF_TOOLS_WORKER_RATIO)), threads, total_pages)
131
-
132
- # 根据实际进程数分组页面范围
133
- pages_per_thread = max(1, total_pages // actual_threads)
134
- page_ranges = []
135
-
136
- for i in range(actual_threads):
137
- range_start = start_page_id + i * pages_per_thread
138
- if i == actual_threads - 1:
139
- # 最后一个进程处理剩余所有页面
140
- range_end = end_page_id
141
- else:
142
- range_end = start_page_id + (i + 1) * pages_per_thread - 1
143
-
144
- page_ranges.append((range_start, range_end))
145
-
146
-
147
- with ProcessPoolExecutor(max_workers=actual_threads) as executor:
148
- # 提交所有任务
149
- futures = []
150
- for range_start, range_end in page_ranges:
151
- future = executor.submit(
152
- _load_images_from_pdf_worker,
153
- pdf_bytes,
154
- dpi,
155
- range_start,
156
- range_end,
157
- image_type,
158
- )
159
- futures.append((range_start, future))
160
-
161
- try:
162
- # 收集结果并按页码排序
163
- all_results = []
164
- for range_start, future in futures:
165
- images_list = future.result(timeout=timeout)
166
- all_results.append((range_start, images_list))
167
-
168
- # 按起始页码排序并合并结果
169
- all_results.sort(key=lambda x: x[0])
170
- images_list = []
171
- for _, imgs in all_results:
172
- images_list.extend(imgs)
173
-
174
- return images_list, pdf_doc
175
- except FuturesTimeoutError:
176
- pdf_doc.close()
177
- executor.shutdown(wait=False, cancel_futures=True)
178
- raise TimeoutError(f"PDF to images conversion timeout after {timeout}s")
179
-
180
-
181
- def load_images_from_pdf_core(
182
- pdf_bytes: bytes,
183
- dpi=200,
184
- start_page_id=0,
185
- end_page_id=None,
186
- image_type=ImageType.PIL, # PIL or BASE64
187
- ):
188
- images_list = []
189
- pdf_doc = pdfium.PdfDocument(pdf_bytes)
190
- pdf_page_num = len(pdf_doc)
191
- end_page_id = get_end_page_id(end_page_id, pdf_page_num)
192
-
193
- for index in range(start_page_id, end_page_id + 1):
194
- # logger.debug(f"Converting page {index}/{pdf_page_num} to image")
195
- page = pdf_doc[index]
196
- image_dict = pdf_page_to_image(page, dpi=dpi, image_type=image_type)
197
- images_list.append(image_dict)
198
-
199
- pdf_doc.close()
200
-
201
- return images_list
202
-
203
-
204
- def cut_image(
205
- bbox: tuple,
206
- page_num: int,
207
- page_pil_img,
208
- return_path,
209
- image_writer,
210
- scale=2,
211
- ):
212
- filename = f"{page_num}_{int(bbox[0])}_{int(bbox[1])}_{int(bbox[2])}_{int(bbox[3])}"
213
- rel_img_path = f"{return_path}_{filename}.jpeg" if return_path is not None else None
214
- crop_img = get_crop_img(bbox, page_pil_img, scale=scale)
215
- img_bytes = image_to_bytes(crop_img, image_format="JPEG")
216
- image_writer.add_image(img_bytes, rel_img_path)
217
- return rel_img_path
218
-
219
-
220
- def get_crop_img(bbox: tuple, pil_img, scale=2):
221
- scale_bbox = (
222
- int(bbox[0] * scale),
223
- int(bbox[1] * scale),
224
- int(bbox[2] * scale),
225
- int(bbox[3] * scale),
226
- )
227
- return pil_img.crop(scale_bbox)
228
-
229
-
230
- # def get_crop_np_img(bbox: tuple, input_img, scale=2):
231
- # if isinstance(input_img, Image.Image):
232
- # np_img = np.asarray(input_img)
233
- # elif isinstance(input_img, np.ndarray):
234
- # np_img = input_img
235
- # else:
236
- # raise ValueError("Input must be a pillow object or a numpy array.")
237
-
238
- # scale_bbox = (
239
- # int(bbox[0] * scale),
240
- # int(bbox[1] * scale),
241
- # int(bbox[2] * scale),
242
- # int(bbox[3] * scale),
243
- # )
244
-
245
- # return np_img[scale_bbox[1] : scale_bbox[3], scale_bbox[0] : scale_bbox[2]]
246
-
247
-
248
- def images_bytes_to_pdf_bytes(image_bytes):
249
- pdf_buffer = BytesIO()
250
- image = Image.open(BytesIO(image_bytes))
251
- image = ImageOps.exif_transpose(image) or image
252
- if image.mode != "RGB":
253
- image = image.convert("RGB")
254
- # 第一张图保存为 PDF,其余追加
255
- image.save(
256
- pdf_buffer,
257
- format="PDF",
258
- # save_all=True
259
- )
260
- # 获取 PDF bytes 并重置指针(可选)
261
- pdf_bytes = pdf_buffer.getvalue()
262
- pdf_buffer.close()
263
- return pdf_bytes
264
-
265
- def get_page_size(page):
266
- w, h = page.get_size()
267
- return (w, h)
268
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/pdf_page_id.py DELETED
@@ -1,10 +0,0 @@
1
-
2
- from loguru import logger
3
-
4
-
5
- def get_end_page_id(end_page_id, pdf_page_num):
6
- end_page_id = end_page_id if end_page_id is not None and end_page_id >= 0 else pdf_page_num - 1
7
- if end_page_id > pdf_page_num - 1:
8
- logger.warning("end_page_id is out of range, use images length")
9
- end_page_id = pdf_page_num - 1
10
- return end_page_id
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/pdf_reader.py DELETED
@@ -1,111 +0,0 @@
1
-
2
- import base64
3
- from io import BytesIO
4
-
5
- from loguru import logger
6
- from PIL import Image
7
- import NaviOCR.config as CONFIG
8
- from pypdfium2 import PdfBitmap, PdfDocument, PdfPage
9
-
10
-
11
- def page_to_image(
12
- page: PdfPage,
13
- dpi: int = 200,
14
- max_width_or_height: int = 3500, # changed from 4500 to 3500
15
- ) -> (Image.Image, float):
16
- scale = dpi / 72
17
-
18
- long_side_length = max(*page.get_size())
19
- if (long_side_length*scale) > max_width_or_height:
20
- scale = max_width_or_height / long_side_length
21
-
22
- bitmap: PdfBitmap = page.render(scale=scale) # type: ignore
23
-
24
- image = bitmap.to_pil()
25
- try:
26
- bitmap.close()
27
- except Exception as e:
28
- logger.error(f"Failed to close bitmap: {e}")
29
- return image, scale
30
-
31
-
32
- def image_to_bytes(
33
- image: Image.Image,
34
- # image_format: str = "PNG", # 也可以用 "JPEG"
35
- image_format: str = "JPEG",
36
- ) -> bytes:
37
- with BytesIO() as image_buffer:
38
- image.save(image_buffer, format=image_format)
39
- return image_buffer.getvalue()
40
-
41
-
42
- def image_to_b64str(
43
- image: Image.Image,
44
- # image_format: str = "PNG", # 也可以用 "JPEG"
45
- image_format: str = "JPEG",
46
- ) -> str:
47
- image_bytes = image_to_bytes(image, image_format)
48
- return base64.b64encode(image_bytes).decode("utf-8")
49
-
50
-
51
- def base64_to_pil_image(
52
- base64_str: str,
53
- ) -> Image.Image:
54
- """Convert base64 string to PIL Image."""
55
- image_bytes = base64.b64decode(base64_str)
56
- with BytesIO(image_bytes) as image_buffer:
57
- return Image.open(image_buffer).convert("RGB")
58
-
59
-
60
- def pdf_to_images(
61
- pdf: str | bytes | PdfDocument,
62
- dpi: int = 200,
63
- max_width_or_height: int = 3500,
64
- start_page_id: int = 0,
65
- end_page_id: int | None = None,
66
- ) -> list[Image.Image]:
67
- doc = pdf if isinstance(pdf, PdfDocument) else PdfDocument(pdf)
68
- page_num = len(doc)
69
-
70
- end_page_id = end_page_id if end_page_id is not None and end_page_id >= 0 else page_num - 1
71
- if end_page_id > page_num - 1:
72
- logger.warning("end_page_id is out of range, use images length")
73
- end_page_id = page_num - 1
74
-
75
- images = []
76
- try:
77
- for i in range(start_page_id, end_page_id + 1):
78
- image, _ = page_to_image(doc[i], dpi, max_width_or_height)
79
- images.append(image)
80
- finally:
81
- try:
82
- doc.close()
83
- except Exception:
84
- pass
85
- return images
86
-
87
-
88
- def pdf_to_images_bytes(
89
- pdf: str | bytes | PdfDocument,
90
- dpi: int = 200,
91
- max_width_or_height: int = 3500,
92
- start_page_id: int = 0,
93
- end_page_id: int | None = None,
94
- # image_format: str = "PNG", # 也可以用 "JPEG"
95
- image_format: str = "JPEG",
96
- ) -> list[bytes]:
97
- images = pdf_to_images(pdf, dpi, max_width_or_height, start_page_id, end_page_id)
98
- return [image_to_bytes(image, image_format) for image in images]
99
-
100
-
101
- def pdf_to_images_b64strs(
102
- pdf: str | bytes | PdfDocument,
103
- dpi: int = 200,
104
- max_width_or_height: int = 3500,
105
- start_page_id: int = 0,
106
- end_page_id: int | None = None,
107
- # image_format: str = "PNG", # 也可以用 "JPEG"
108
- image_format: str = "JPEG",
109
- ) -> list[str]:
110
- images = pdf_to_images(pdf, dpi, max_width_or_height, start_page_id, end_page_id)
111
- return [image_to_b64str(image, image_format) for image in images]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/tools/read_file.py DELETED
@@ -1,21 +0,0 @@
1
- from .guess_suffix_or_lang import guess_suffix_by_bytes
2
- from .pdf_image_tools import images_bytes_to_pdf_bytes
3
- from pathlib import Path
4
-
5
- pdf_suffixes = ["pdf"]
6
- image_suffixes = ["png", "jpeg", "jp2", "webp", "gif", "bmp", "jpg", "tiff"]
7
-
8
-
9
-
10
- def read_fn(path):
11
- if not isinstance(path, Path):
12
- path = Path(path)
13
- with open(str(path), "rb") as input_file:
14
- file_bytes = input_file.read()
15
- file_suffix = guess_suffix_by_bytes(file_bytes, path)
16
- if file_suffix in image_suffixes:
17
- return images_bytes_to_pdf_bytes(file_bytes)
18
- elif file_suffix in pdf_suffixes:
19
- return file_bytes
20
- else:
21
- raise Exception(f"Unknown file suffix: {file_suffix}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/version.py DELETED
@@ -1 +0,0 @@
1
- __version__='1.0.0'
 
 
NaviOCR/vlm_utils/NaviOCR_model.py DELETED
@@ -1,126 +0,0 @@
1
- import os
2
- from .NaviOCR_client import NaviOCRClient
3
- import NaviOCR.config as CONFIG
4
-
5
-
6
- class NaviOCRMODEL:
7
- _instance = None
8
- _models = {}
9
- def __new__(cls, *args, **kwargs):
10
- if cls._instance is None:
11
- cls._instance = super().__new__(cls)
12
- return cls._instance
13
-
14
- def get_model(
15
- self,
16
- backend: str,
17
- model_path: str | None,
18
- server_url: str | None,
19
- **kwargs,
20
- ) -> NaviOCRClient:
21
- print(backend, model_path, server_url)
22
- key = (backend, model_path, server_url)
23
- print(key not in self._models)
24
- if key not in self._models:
25
- model = None
26
- processor = None
27
- vllm_llm = None
28
- lmdeploy_engine = None
29
- vllm_async_llm = None
30
- batch_size = 0
31
- max_concurrency = 100
32
- http_timeout = 600
33
- server_headers = None
34
- max_retries = 3
35
- retry_backoff_factor = 0.5
36
- if os.getenv('OMP_NUM_THREADS') is None:
37
- os.environ["OMP_NUM_THREADS"] = "1"
38
- if backend == "transformers":
39
- if not model_path:
40
- raise ValueError("model_path must be provided for the transformers backend.")
41
- try:
42
- import torch
43
- from transformers import AutoProcessor
44
- try:
45
- from transformers import AutoModelForImageTextToText as AutoModelClass
46
- except ImportError:
47
- try:
48
- from transformers import AutoModelForVision2Seq as AutoModelClass
49
- except ImportError:
50
- from transformers import AutoModel as AutoModelClass
51
- except ImportError as exc:
52
- raise ImportError("Please install torch and transformers to use the transformers backend.") from exc
53
-
54
- processor = AutoProcessor.from_pretrained(
55
- model_path,
56
- trust_remote_code=True,
57
- )
58
- torch_dtype = kwargs.pop(
59
- "torch_dtype",
60
- torch.bfloat16 if torch.cuda.is_available() else torch.float32,
61
- )
62
- model = AutoModelClass.from_pretrained(
63
- model_path,
64
- trust_remote_code=True,
65
- torch_dtype=torch_dtype,
66
- **kwargs,
67
- )
68
- if torch.cuda.is_available():
69
- model = model.cuda()
70
- model = model.eval()
71
- if backend == "vllm-engine":
72
- try:
73
- import vllm
74
- except ImportError:
75
- raise ImportError("Please install vllm to use the vllm-engine backend.")
76
- if "gpu_memory_utilization" not in kwargs:
77
- kwargs["gpu_memory_utilization"] = CONFIG.GPU_MEMORY_UTILIZATION
78
- if "model" not in kwargs:
79
- kwargs["model"] = model_path
80
- if "logits_processors" not in kwargs:
81
- from . import LogitsProcessor
82
- kwargs["logits_processors"] = [LogitsProcessor]
83
- if 'max_model_len' not in kwargs:
84
- kwargs["max_model_len"] = CONFIG.MAX_MODEL_LEN
85
- vllm_llm = vllm.LLM(**kwargs)
86
-
87
- elif backend == "vllm-async-engine":
88
- try:
89
- from vllm.engine.arg_utils import AsyncEngineArgs
90
- from vllm.v1.engine.async_llm import AsyncLLM
91
- except ImportError:
92
- raise ImportError("Please install vllm to use the vllm-async-engine backend.")
93
-
94
- if "gpu_memory_utilization" not in kwargs:
95
- kwargs["gpu_memory_utilization"] = CONFIG.GPU_MEMORY_UTILIZATION
96
-
97
- if "model" not in kwargs:
98
- kwargs["model"] = model_path
99
-
100
- if "logits_processors" not in kwargs:
101
- from . import NaviOCRLogitsProcessor
102
- kwargs["logits_processors"] = [NaviOCRLogitsProcessor]
103
-
104
- if 'max_model_len' not in kwargs:
105
- kwargs["max_model_len"] = CONFIG.MAX_MODEL_LEN
106
- vllm_async_llm = AsyncLLM.from_engine_args(AsyncEngineArgs(**kwargs))
107
-
108
- self._models[key] = NaviOCRClient(
109
- backend=backend,
110
- model=model,
111
- processor=processor,
112
- lmdeploy_engine=lmdeploy_engine,
113
- vllm_llm=vllm_llm,
114
- vllm_async_llm=vllm_async_llm,
115
- server_url=server_url,
116
- batch_size=batch_size,
117
- max_concurrency=max_concurrency,
118
- http_timeout=http_timeout,
119
- server_headers=server_headers,
120
- max_retries=max_retries,
121
- retry_backoff_factor=retry_backoff_factor,
122
- )
123
- return self._models[key]
124
-
125
-
126
- NaviOCRMODEL_SERVICE = NaviOCRMODEL()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/vlm_utils/version.py DELETED
@@ -1 +0,0 @@
1
- __version__ = "0.1.21"
 
 
NaviOCR/vlm_utils/vlm_client/vllm_async_engine_client.py DELETED
@@ -1,259 +0,0 @@
1
- import asyncio
2
- import uuid
3
- from io import BytesIO
4
- from typing import TYPE_CHECKING, Sequence
5
-
6
- if TYPE_CHECKING:
7
- from vllm.outputs import RequestOutput
8
-
9
- from PIL import Image
10
-
11
- from .base_client import (
12
- DEFAULT_SYSTEM_PROMPT,
13
- DEFAULT_USER_PROMPT,
14
- RequestError,
15
- SamplingParams,
16
- ServerError,
17
- UnsupportedError,
18
- VlmClient,
19
- )
20
- from .utils import aio_load_resource, gather_tasks, get_rgb_image
21
-
22
-
23
- class VllmAsyncEngineVlmClient(VlmClient):
24
- def __init__(
25
- self,
26
- vllm_async_llm, # vllm.v1.engine.async_llm.AsyncLLM instance
27
- prompt: str = DEFAULT_USER_PROMPT,
28
- system_prompt: str = DEFAULT_SYSTEM_PROMPT,
29
- sampling_params: SamplingParams | None = None,
30
- text_before_image: bool = False,
31
- allow_truncated_content: bool = False,
32
- max_concurrency: int = 100,
33
- debug: bool = False,
34
- ):
35
- super().__init__(
36
- prompt=prompt,
37
- system_prompt=system_prompt,
38
- sampling_params=sampling_params,
39
- text_before_image=text_before_image,
40
- allow_truncated_content=allow_truncated_content,
41
- )
42
-
43
- try:
44
- from vllm import SamplingParams
45
- from vllm.sampling_params import RequestOutputKind
46
- from vllm.v1.engine.async_llm import AsyncLLM
47
- except ImportError:
48
- raise ImportError("Please install vllm to use VllmEngineVlmClient.")
49
-
50
- if not vllm_async_llm:
51
- raise ValueError("vllm_async_llm is None.")
52
- if not isinstance(vllm_async_llm, AsyncLLM):
53
- raise ValueError(f"vllm_async_llm must be an instance of {AsyncLLM}")
54
-
55
- self.vllm_async_llm = vllm_async_llm
56
- if vllm_async_llm.tokenizer is None:
57
- raise ValueError("vllm_async_llm.tokenizer is None.")
58
-
59
- tokenizer = vllm_async_llm.tokenizer
60
- if hasattr(tokenizer, "get_lora_tokenizer"):
61
- tokenizer = tokenizer.get_lora_tokenizer() # type: ignore
62
-
63
- self.tokenizer = tokenizer
64
- self.model_max_length = vllm_async_llm.model_config.max_model_len
65
- self.VllmSamplingParams = SamplingParams
66
- self.VllmRequestOutputKind = RequestOutputKind
67
- self.max_concurrency = max_concurrency
68
- self.debug = debug
69
-
70
- def build_messages(self, prompt: str) -> list[dict]:
71
- prompt = prompt or self.prompt
72
- messages = []
73
- if self.system_prompt:
74
- messages.append({"role": "system", "content": self.system_prompt})
75
- if "<image>" in prompt:
76
- prompt_1, prompt_2 = prompt.split("<image>", 1)
77
- user_messages = [
78
- *([{"type": "text", "text": prompt_1}] if prompt_1.strip() else []),
79
- {"type": "image"},
80
- *([{"type": "text", "text": prompt_2}] if prompt_2.strip() else []),
81
- ]
82
- elif self.text_before_image:
83
- user_messages = [
84
- {"type": "text", "text": prompt},
85
- {"type": "image"},
86
- ]
87
- else: # image before text, which is the default behavior.
88
- user_messages = [
89
- {"type": "image"},
90
- {"type": "text", "text": prompt},
91
- ]
92
- messages.append({"role": "user", "content": user_messages})
93
- return messages
94
-
95
- def build_vllm_sampling_params(self, sampling_params: SamplingParams | None):
96
- sp = self.build_sampling_params(sampling_params)
97
-
98
- vllm_sp_dict = {
99
- "temperature": sp.temperature,
100
- "top_p": sp.top_p,
101
- "top_k": sp.top_k,
102
- "presence_penalty": sp.presence_penalty,
103
- "frequency_penalty": sp.frequency_penalty,
104
- "repetition_penalty": sp.repetition_penalty,
105
- # max_tokens should smaller than model max length
106
- "max_tokens": sp.max_new_tokens if sp.max_new_tokens is not None else self.model_max_length,
107
- }
108
-
109
- if sp.no_repeat_ngram_size is not None:
110
- vllm_sp_dict["extra_args"] = {
111
- "no_repeat_ngram_size": sp.no_repeat_ngram_size,
112
- "debug": self.debug,
113
- }
114
-
115
- return self.VllmSamplingParams(
116
- **{k: v for k, v in vllm_sp_dict.items() if v is not None},
117
- skip_special_tokens=False,
118
- output_kind=self.VllmRequestOutputKind.FINAL_ONLY,
119
- )
120
-
121
- def get_output_content(self, output: "RequestOutput") -> str:
122
- if not output.finished:
123
- raise ServerError("The output generation was not finished.")
124
-
125
- choices = output.outputs
126
- if not (isinstance(choices, list) and choices):
127
- raise ServerError("No choices found in the output.")
128
-
129
- finish_reason = choices[0].finish_reason
130
- if finish_reason is None:
131
- raise ServerError("Finish reason is None in the output.")
132
- if finish_reason == "length":
133
- if not self.allow_truncated_content:
134
- raise RequestError("The output was truncated due to length limit.")
135
- else:
136
- print("Warning: The output was truncated due to length limit.")
137
- elif finish_reason != "stop":
138
- raise RequestError(f"Unexpected finish reason: {finish_reason}")
139
-
140
- return choices[0].text
141
-
142
- def predict(
143
- self,
144
- image: Image.Image | bytes | str,
145
- prompt: str = "",
146
- sampling_params: SamplingParams | None = None,
147
- priority: int | None = None,
148
- ) -> str:
149
- raise UnsupportedError(
150
- "Synchronous predict() is not supported in vllm-async-engine VlmClient(backend). "
151
- "Please use aio_predict() instead. If you intend to use synchronous client, "
152
- "please use vllm-engine VlmClient(backend)."
153
- )
154
-
155
- def batch_predict(
156
- self,
157
- images: Sequence[Image.Image | bytes | str],
158
- prompts: Sequence[str] | str = "",
159
- sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
160
- priority: Sequence[int | None] | int | None = None,
161
- ) -> list[str]:
162
- raise UnsupportedError(
163
- "Synchronous batch_predict() is not supported in vllm-async-engine VlmClient(backend). "
164
- "Please use aio_batch_predict() instead. If you intend to use synchronous client, "
165
- "please use vllm-engine VlmClient(backend)."
166
- )
167
-
168
- async def aio_predict(
169
- self,
170
- image: Image.Image | bytes | str,
171
- prompt: str = "",
172
- sampling_params: SamplingParams | None = None,
173
- priority: int | None = None,
174
- ) -> str:
175
- if isinstance(image, str):
176
- image = await aio_load_resource(image)
177
- if not isinstance(image, Image.Image):
178
- image = Image.open(BytesIO(image))
179
- image = get_rgb_image(image)
180
-
181
- chat_prompt: str = self.tokenizer.apply_chat_template(
182
- self.build_messages(prompt), # type: ignore
183
- tokenize=False,
184
- add_generation_prompt=True,
185
- )
186
-
187
- vllm_sp = self.build_vllm_sampling_params(sampling_params)
188
-
189
- generate_kwargs = {}
190
- if priority is not None:
191
- generate_kwargs["priority"] = priority
192
-
193
- last_output = None
194
- async for output in self.vllm_async_llm.generate(
195
- prompt={"prompt": chat_prompt, "multi_modal_data": {"image": image}},
196
- sampling_params=vllm_sp,
197
- request_id=str(uuid.uuid4()),
198
- **generate_kwargs,
199
- ):
200
- last_output = output
201
-
202
- if last_output is None: # this should not happen
203
- raise ServerError("No output from the server.")
204
- result = self.get_output_content(last_output)
205
- # if 'table' in prompt:
206
- # print(result)
207
- return result
208
-
209
- async def aio_batch_predict(
210
- self,
211
- images: Sequence[Image.Image | bytes | str],
212
- prompts: Sequence[str] | str = "",
213
- sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
214
- priority: Sequence[int | None] | int | None = None,
215
- semaphore: asyncio.Semaphore | None = None,
216
- use_tqdm=False,
217
- tqdm_desc: str | None = None,
218
- ) -> list[str]:
219
- if isinstance(prompts, str):
220
- prompts = [prompts] * len(images)
221
- if not isinstance(sampling_params, Sequence):
222
- sampling_params = [sampling_params] * len(images)
223
- if not isinstance(priority, Sequence):
224
- priority = [priority] * len(images)
225
-
226
- assert len(prompts) == len(images), "Length of prompts and images must match."
227
- assert len(sampling_params) == len(images), "Length of sampling_params and images must match."
228
- assert len(priority) == len(images), "Length of priority and images must match."
229
-
230
- if semaphore is None:
231
- semaphore = asyncio.Semaphore(self.max_concurrency)
232
-
233
- async def predict_with_semaphore(
234
- image: Image.Image | bytes | str,
235
- prompt: str,
236
- sampling_params: SamplingParams | None,
237
- priority: int | None,
238
- ):
239
- async with semaphore:
240
- return await self.aio_predict(
241
- image=image,
242
- prompt=prompt,
243
- sampling_params=sampling_params,
244
- priority=priority,
245
- )
246
-
247
- return await gather_tasks(
248
- tasks=[
249
- predict_with_semaphore(*args)
250
- for args in zip(
251
- images,
252
- prompts,
253
- sampling_params,
254
- priority,
255
- )
256
- ],
257
- use_tqdm=use_tqdm,
258
- tqdm_desc=tqdm_desc,
259
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/vlm_utils/vlm_client/vllm_engine_client.py DELETED
@@ -1,255 +0,0 @@
1
- import asyncio
2
- from io import BytesIO
3
- from typing import TYPE_CHECKING, Sequence
4
-
5
- if TYPE_CHECKING:
6
- from vllm.outputs import RequestOutput
7
- from vllm.sampling_params import SamplingParams as VllmSamplingParams
8
-
9
- from PIL import Image
10
-
11
- from .base_client import (
12
- DEFAULT_SYSTEM_PROMPT,
13
- DEFAULT_USER_PROMPT,
14
- RequestError,
15
- SamplingParams,
16
- ServerError,
17
- UnsupportedError,
18
- VlmClient,
19
- )
20
- from .utils import get_rgb_image, load_resource
21
-
22
-
23
- class VllmEngineVlmClient(VlmClient):
24
- def __init__(
25
- self,
26
- vllm_llm, # vllm.LLM instance
27
- prompt: str = DEFAULT_USER_PROMPT,
28
- system_prompt: str = DEFAULT_SYSTEM_PROMPT,
29
- sampling_params: SamplingParams | None = None,
30
- text_before_image: bool = False,
31
- allow_truncated_content: bool = False,
32
- batch_size: int = 0,
33
- use_tqdm: bool = True,
34
- debug: bool = False,
35
- ):
36
- super().__init__(
37
- prompt=prompt,
38
- system_prompt=system_prompt,
39
- sampling_params=sampling_params,
40
- text_before_image=text_before_image,
41
- allow_truncated_content=allow_truncated_content,
42
- )
43
-
44
- try:
45
- from vllm import LLM, SamplingParams
46
- except ImportError:
47
- raise ImportError("Please install vllm to use VllmEngineVlmClient.")
48
-
49
- if not vllm_llm:
50
- raise ValueError("vllm_llm is None.")
51
- if not isinstance(vllm_llm, LLM):
52
- raise ValueError("vllm_llm must be an instance of vllm.LLM.")
53
-
54
- self.vllm_llm = vllm_llm
55
- self.tokenizer = vllm_llm.get_tokenizer()
56
- self.model_max_length = vllm_llm.llm_engine.model_config.max_model_len
57
- self.VllmSamplingParams = SamplingParams
58
- self.batch_size = batch_size
59
- self.use_tqdm = use_tqdm
60
- self.debug = debug
61
-
62
- def build_messages(self, prompt: str) -> list[dict]:
63
- prompt = prompt or self.prompt
64
- messages = []
65
- if self.system_prompt:
66
- messages.append({"role": "system", "content": self.system_prompt})
67
- if "<image>" in prompt:
68
- prompt_1, prompt_2 = prompt.split("<image>", 1)
69
- user_messages = [
70
- *([{"type": "text", "text": prompt_1}] if prompt_1.strip() else []),
71
- {"type": "image"},
72
- *([{"type": "text", "text": prompt_2}] if prompt_2.strip() else []),
73
- ]
74
- elif self.text_before_image:
75
- user_messages = [
76
- {"type": "text", "text": prompt},
77
- {"type": "image"},
78
- ]
79
- else: # image before text, which is the default behavior.
80
- user_messages = [
81
- {"type": "image"},
82
- {"type": "text", "text": prompt},
83
- ]
84
- messages.append({"role": "user", "content": user_messages})
85
- return messages
86
-
87
- def build_vllm_sampling_params(self, sampling_params: SamplingParams | None):
88
- sp = self.build_sampling_params(sampling_params)
89
-
90
- vllm_sp_dict = {
91
- "temperature": sp.temperature,
92
- "top_p": sp.top_p,
93
- "top_k": sp.top_k,
94
- "presence_penalty": sp.presence_penalty,
95
- "frequency_penalty": sp.frequency_penalty,
96
- "repetition_penalty": sp.repetition_penalty,
97
- # max_tokens should smaller than model max length
98
- "max_tokens": sp.max_new_tokens if sp.max_new_tokens is not None else self.model_max_length,
99
- }
100
-
101
- if sp.no_repeat_ngram_size is not None:
102
- vllm_sp_dict["extra_args"] = {
103
- "no_repeat_ngram_size": sp.no_repeat_ngram_size,
104
- "debug": self.debug,
105
- }
106
-
107
- return self.VllmSamplingParams(
108
- **{k: v for k, v in vllm_sp_dict.items() if v is not None},
109
- skip_special_tokens=False,
110
- )
111
-
112
- def get_output_content(self, output: "RequestOutput") -> str:
113
- if not output.finished:
114
- raise ServerError("The output generation was not finished.")
115
-
116
- choices = output.outputs
117
- if not (isinstance(choices, list) and choices):
118
- raise ServerError("No choices found in the output.")
119
-
120
- finish_reason = choices[0].finish_reason
121
- if finish_reason is None:
122
- raise ServerError("Finish reason is None in the output.")
123
- if finish_reason == "length":
124
- if not self.allow_truncated_content:
125
- raise RequestError("The output was truncated due to length limit.")
126
- else:
127
- print("Warning: The output was truncated due to length limit.")
128
- elif finish_reason != "stop":
129
- raise RequestError(f"Unexpected finish reason: {finish_reason}")
130
-
131
- return choices[0].text
132
-
133
- def predict(
134
- self,
135
- image: Image.Image | bytes | str,
136
- prompt: str = "",
137
- sampling_params: SamplingParams | None = None,
138
- priority: int | None = None,
139
- ) -> str:
140
- return self.batch_predict(
141
- [image], # type: ignore
142
- [prompt],
143
- [sampling_params],
144
- )[0]
145
-
146
- def batch_predict(
147
- self,
148
- images: Sequence[Image.Image | bytes | str],
149
- prompts: Sequence[str] | str = "",
150
- sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
151
- priority: Sequence[int | None] | int | None = None,
152
- ) -> list[str]:
153
- if not isinstance(prompts, str):
154
- assert len(prompts) == len(images), "Length of prompts and images must match."
155
- if isinstance(sampling_params, Sequence):
156
- assert len(sampling_params) == len(images), "Length of sampling_params and images must match."
157
- if isinstance(priority, Sequence):
158
- assert len(priority) == len(images), "Length of priority and images must match."
159
-
160
- image_objs: list[Image.Image] = []
161
- for image in images:
162
- if isinstance(image, str):
163
- image = load_resource(image)
164
- if not isinstance(image, Image.Image):
165
- image = Image.open(BytesIO(image))
166
- image = get_rgb_image(image)
167
- image_objs.append(image)
168
-
169
- if isinstance(prompts, str):
170
- chat_prompts: list[str] = [
171
- self.tokenizer.apply_chat_template(
172
- self.build_messages(prompts), # type: ignore
173
- tokenize=False,
174
- add_generation_prompt=True,
175
- )
176
- ] * len(images)
177
- else: # isinstance(prompts, Sequence[str])
178
- chat_prompts: list[str] = [
179
- self.tokenizer.apply_chat_template(
180
- self.build_messages(prompt), # type: ignore
181
- tokenize=False,
182
- add_generation_prompt=True,
183
- )
184
- for prompt in prompts
185
- ]
186
-
187
- if not isinstance(sampling_params, Sequence):
188
- vllm_sp_list = [self.build_vllm_sampling_params(sampling_params)] * len(images)
189
- else:
190
- vllm_sp_list = [self.build_vllm_sampling_params(sp) for sp in sampling_params]
191
-
192
- outputs = []
193
- batch_size = self.batch_size if self.batch_size > 0 else len(images)
194
- batch_size = max(1, batch_size)
195
-
196
- for i in range(0, len(images), batch_size):
197
- batch_image_objs = image_objs[i : i + batch_size]
198
- batch_chat_prompts = chat_prompts[i : i + batch_size]
199
- batch_sp_list = vllm_sp_list[i : i + batch_size]
200
- batch_outputs = self._predict_one_batch(
201
- batch_image_objs,
202
- batch_chat_prompts,
203
- batch_sp_list,
204
- )
205
- outputs.extend(batch_outputs)
206
-
207
- return outputs
208
-
209
- def _predict_one_batch(
210
- self,
211
- image_objs: list[Image.Image],
212
- chat_prompts: list[str],
213
- vllm_sampling_params: list["VllmSamplingParams"],
214
- ):
215
- vllm_prompts = [
216
- {"prompt": chat_prompt, "multi_modal_data": {"image": image}}
217
- for chat_prompt, image in zip(chat_prompts, image_objs)
218
- ]
219
-
220
- outputs = self.vllm_llm.generate(
221
- prompts=vllm_prompts, # type: ignore
222
- sampling_params=vllm_sampling_params,
223
- use_tqdm=self.use_tqdm,
224
- )
225
-
226
- return [self.get_output_content(output) for output in outputs]
227
-
228
- async def aio_predict(
229
- self,
230
- image: Image.Image | bytes | str,
231
- prompt: str = "",
232
- sampling_params: SamplingParams | None = None,
233
- priority: int | None = None,
234
- ) -> str:
235
- raise UnsupportedError(
236
- "Asynchronous aio_predict() is not supported in vllm-engine VlmClient(backend). "
237
- "Please use predict() instead. If you intend to use asynchronous client, "
238
- "please use vllm-async-engine VlmClient(backend)."
239
- )
240
-
241
- async def aio_batch_predict(
242
- self,
243
- images: Sequence[Image.Image | bytes | str],
244
- prompts: Sequence[str] | str = "",
245
- sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
246
- priority: Sequence[int | None] | int | None = None,
247
- semaphore: asyncio.Semaphore | None = None,
248
- use_tqdm=False,
249
- tqdm_desc: str | None = None,
250
- ) -> list[str]:
251
- raise UnsupportedError(
252
- "Asynchronous aio_batch_predict() is not supported in vllm-engine VlmClient(backend). "
253
- "Please use batch_predict() instead. If you intend to use asynchronous client, "
254
- "please use vllm-async-engine VlmClient(backend)."
255
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
NaviOCR/vlm_utils/vlm_client/vllm_v1_no_repeat_ngram.py DELETED
@@ -1,93 +0,0 @@
1
- # Logits Processor for vLLM V1 Engine.
2
-
3
- from typing import Any
4
-
5
- import torch
6
- from vllm.config import VllmConfig
7
-
8
- try:
9
- from vllm.v1.sample.logits_processor.interface import (
10
- BatchUpdate,
11
- LogitsProcessor,
12
- MoveDirectionality,
13
- )
14
- except ImportError as e:
15
- raise ImportError("Please install vllm>=0.10.1 to use this feature.") from e
16
-
17
-
18
- def _get_int_value(extra_args: dict[str, Any] | None, key: str) -> int | None:
19
- if isinstance(extra_args, dict):
20
- arg_value = extra_args.get(key)
21
- if arg_value is not None:
22
- try:
23
- return int(arg_value)
24
- except Exception:
25
- pass
26
- return None
27
-
28
-
29
- class VllmV1NoRepeatNGramLogitsProcessor(LogitsProcessor):
30
- """
31
- Prevents repeating the same n-gram of specified size in the output.
32
- Inspired by Hugging Face's NoRepeatNGramLogitsProcessor.
33
- Handled Extra Args:
34
- no_repeat_ngram_size (int): Size of the n-gram to avoid repeating.
35
- """
36
-
37
- def __init__(self, vllm_config: VllmConfig, device: torch.device, is_pin_memory: bool):
38
- # mapping: index -> (no_repeat_ngram_size, output_tok_ids, cached_ngrams)
39
- self.req_info: dict[int, tuple[int, list[int], dict[tuple, list[int]]]] = {}
40
-
41
- def is_argmax_invariant(self) -> bool:
42
- return False
43
-
44
- def update_state(self, batch_update: BatchUpdate | None) -> None:
45
- if not batch_update:
46
- return
47
-
48
- for index in batch_update.removed:
49
- self.req_info.pop(index, None)
50
-
51
- for index, params, _, output_tok_ids in batch_update.added:
52
- val = _get_int_value(params.extra_args, "no_repeat_ngram_size")
53
- no_repeat_ngram_size = 0 if (val is None or val < 0) else val
54
- if isinstance(params.extra_args, dict) and params.extra_args.get("debug"):
55
- print(f"Request {index}: no_repeat_ngram_size = {no_repeat_ngram_size}")
56
- self.req_info[index] = (no_repeat_ngram_size, output_tok_ids, {})
57
-
58
- for a_index, b_index, direct in batch_update.moved:
59
- a_info = self.req_info.pop(a_index, None)
60
- b_info = self.req_info.pop(b_index, None)
61
- if a_info is not None:
62
- self.req_info[b_index] = a_info
63
- if direct == MoveDirectionality.SWAP and b_info is not None:
64
- self.req_info[a_index] = b_info
65
-
66
- def apply(self, logits: torch.Tensor) -> torch.Tensor:
67
- for index in range(len(logits)):
68
- req_info = self.req_info.get(index)
69
- if req_info is None:
70
- continue
71
- no_repeat_ngram_size, output_tok_ids, cached_ngrams = req_info
72
- if no_repeat_ngram_size <= 0:
73
- continue
74
- # Skip if there are not enough tokens to form an n-gram
75
- if len(output_tok_ids) < no_repeat_ngram_size:
76
- continue
77
-
78
- # Get the n-gram prefix (all but the last token)
79
- prev_ngram = tuple(output_tok_ids[-no_repeat_ngram_size:-1])
80
- last_token = output_tok_ids[-1]
81
-
82
- # Store this n-gram occurrence
83
- cached_ngrams.setdefault(prev_ngram, []).append(last_token)
84
-
85
- # Get the next-token candidates to ban based on current prefix
86
- current_prefix = tuple(output_tok_ids[-no_repeat_ngram_size + 1 :])
87
- banned_tokens = cached_ngrams.get(current_prefix, [])
88
-
89
- # Set the logits of banned tokens to negative infinity
90
- for token in banned_tokens:
91
- logits[index][token] = -float("inf")
92
-
93
- return logits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
examples/layout.jpg DELETED

Git LFS Details

  • SHA256: 2ae27f3a9284810bf7ce479ac8921359ccf00b7ebee1017b5e8da3e28ebe4af4
  • Pointer size: 131 Bytes
  • Size of remote file: 749 kB
examples/layout_distorted.jpg DELETED

Git LFS Details

  • SHA256: 86000b5847dca0464838c9672650b7688fa942d92c241b2ec778ca613cd25dcd
  • Pointer size: 131 Bytes
  • Size of remote file: 613 kB
examples/text.png DELETED
Binary file (62 kB)