output_formatter_v2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. """
  2. 统一输出格式化器 v2
  3. 严格遵循 MinerU mineru_vllm_results_cell_bbox 格式
  4. 支持:
  5. 1. MinerU 标准 middle.json 格式(用于 union_make 生成 Markdown)
  6. 2. mineru_vllm_results_cell_bbox 格式(每页独立 JSON)
  7. 3. Markdown 输出(复用 MinerU union_make)
  8. 4. Debug 模式:layout 图片、OCR 图片
  9. 5. 表格 HTML 输出(带坐标信息)
  10. 6. 金额数字标准化(全角→半角转换)
  11. 模块结构:
  12. - json_formatters.py: JSON 格式化工具
  13. - markdown_generator.py: Markdown 生成器
  14. - html_generator.py: HTML 生成器
  15. - visualization_utils.py: 可视化工具
  16. """
  17. import json
  18. import sys
  19. import numpy as np
  20. from pathlib import Path
  21. from typing import Dict, Any, List, Optional
  22. from loguru import logger
  23. # 导入子模块
  24. from .json_formatters import JSONFormatters
  25. from .markdown_generator import MarkdownGenerator
  26. from .html_generator import HTMLGenerator
  27. from .visualization_utils import VisualizationUtils
  28. # 导入数字标准化工具
  29. from .normalize_financial_numbers import normalize_markdown_table, normalize_json_table
  30. class NumpyEncoder(json.JSONEncoder):
  31. """自定义JSON编码器,处理numpy类型"""
  32. def default(self, obj):
  33. if isinstance(obj, np.integer):
  34. return int(obj)
  35. elif isinstance(obj, np.floating):
  36. return float(obj)
  37. elif isinstance(obj, np.ndarray):
  38. return obj.tolist()
  39. return super().default(obj)
  40. class OutputFormatterV2:
  41. """
  42. 统一输出格式化器
  43. 严格遵循 MinerU mineru_vllm_results_cell_bbox 格式:
  44. - middle.json: MinerU 标准格式,用于生成 Markdown
  45. - page_xxx.json: 每页独立的 JSON,包含 table_cells
  46. - Markdown: 带 bbox 注释
  47. - 表格: HTML 格式,带 data-bbox 属性
  48. 命名规则:
  49. - PDF输入: 文件名_page_001.*(按页编号)
  50. - 图片输入: 文件名.*(不加页码后缀)
  51. """
  52. # 颜色映射(导出供其他模块使用)
  53. COLOR_MAP = VisualizationUtils.COLOR_MAP
  54. OCR_BOX_COLOR = VisualizationUtils.OCR_BOX_COLOR
  55. CELL_BOX_COLOR = VisualizationUtils.CELL_BOX_COLOR
  56. def __init__(self, output_dir: str):
  57. """
  58. 初始化格式化器
  59. Args:
  60. output_dir: 输出目录
  61. """
  62. self.output_dir = Path(output_dir)
  63. self.output_dir.mkdir(parents=True, exist_ok=True)
  64. @staticmethod
  65. def is_pdf_input(results: Dict[str, Any]) -> bool:
  66. """
  67. 判断输入是否为 PDF
  68. Args:
  69. results: 处理结果
  70. Returns:
  71. True 如果输入是 PDF,否则 False
  72. """
  73. doc_path = results.get('document_path', '')
  74. if doc_path:
  75. return Path(doc_path).suffix.lower() == '.pdf'
  76. # 如果没有 document_path,检查 metadata
  77. input_type = results.get('metadata', {}).get('input_type', '')
  78. return input_type == 'pdf'
  79. @staticmethod
  80. def get_page_name(doc_name: str, page_idx: int, is_pdf: bool, total_pages: int = 1) -> str:
  81. """
  82. 获取页面名称
  83. Args:
  84. doc_name: 文档名称
  85. page_idx: 页码索引(从0开始)
  86. is_pdf: 是否为 PDF 输入
  87. total_pages: 总页数
  88. Returns:
  89. 页面名称(不含扩展名)
  90. """
  91. if is_pdf or total_pages > 1:
  92. # PDF 或多页输入:添加页码后缀
  93. return f"{doc_name}_page_{page_idx + 1:03d}"
  94. else:
  95. # 单个图片:不添加页码后缀
  96. return doc_name
  97. def save_results(
  98. self,
  99. results: Dict[str, Any],
  100. output_config: Dict[str, Any]
  101. ) -> Dict[str, Any]:
  102. """
  103. 保存处理结果
  104. 命名规则:
  105. - PDF输入: 文件名_page_001.*(按页编号)
  106. - 图片输入: 文件名.*(不加页码后缀)
  107. Args:
  108. results: 处理结果
  109. output_config: 输出配置,支持以下选项:
  110. - create_subdir: 是否在输出目录下创建文档名子目录(默认 False)
  111. - ... 其他选项见 save_mineru_format 函数
  112. Returns:
  113. 输出文件路径字典
  114. """
  115. output_paths: Dict[str, Any] = {
  116. 'images': [],
  117. 'json_pages': [],
  118. }
  119. # 创建文档输出目录
  120. doc_name = Path(results['document_path']).stem
  121. # 是否创建子目录(默认不创建,直接使用指定的输出目录)
  122. create_subdir = output_config.get('create_subdir', False)
  123. if create_subdir:
  124. doc_output_dir = self.output_dir / doc_name
  125. else:
  126. doc_output_dir = self.output_dir
  127. doc_output_dir.mkdir(parents=True, exist_ok=True)
  128. # 判断输入类型
  129. is_pdf = self.is_pdf_input(results)
  130. total_pages = len(results.get('pages', []))
  131. # 创建 images 子目录
  132. images_dir = doc_output_dir / 'images'
  133. images_dir.mkdir(exist_ok=True)
  134. # 1. 首先保存图片元素(设置 image_path)
  135. image_paths = VisualizationUtils.save_image_elements(
  136. results, images_dir, doc_name, is_pdf=is_pdf
  137. )
  138. if image_paths:
  139. output_paths['images'] = image_paths
  140. # 2. 转换为 MinerU middle.json 格式
  141. middle_json = JSONFormatters.convert_to_middle_json(results)
  142. # 3. 保存 middle.json
  143. if output_config.get('save_json', True):
  144. json_path = doc_output_dir / f"{doc_name}_middle.json"
  145. json_content = json.dumps(middle_json, ensure_ascii=False, indent=2, cls=NumpyEncoder)
  146. # 金额数字标准化
  147. normalize_numbers = output_config.get('normalize_numbers', True)
  148. if normalize_numbers:
  149. original_content = json_content
  150. json_content = normalize_json_table(json_content)
  151. # 检查是否有变化
  152. if json_content != original_content:
  153. # 保存原始文件
  154. original_path = doc_output_dir / f"{doc_name}_middle_original.json"
  155. with open(original_path, 'w', encoding='utf-8') as f:
  156. f.write(original_content)
  157. logger.info(f"📄 Original middle JSON saved: {original_path}")
  158. output_paths['middle_json_original'] = str(original_path)
  159. with open(json_path, 'w', encoding='utf-8') as f:
  160. f.write(json_content)
  161. output_paths['middle_json'] = str(json_path)
  162. logger.info(f"📄 Middle JSON saved: {json_path}")
  163. # 4. 保存每页独立的 mineru_vllm_results_cell_bbox 格式 JSON
  164. if output_config.get('save_page_json', True):
  165. normalize_numbers = output_config.get('normalize_numbers', True)
  166. page_json_paths = JSONFormatters.save_page_jsons(
  167. results, doc_output_dir, doc_name, is_pdf=is_pdf,
  168. normalize_numbers=normalize_numbers
  169. )
  170. output_paths['json_pages'] = page_json_paths
  171. # 5. 保存 Markdown(完整版)
  172. if output_config.get('save_markdown', True):
  173. normalize_numbers = output_config.get('normalize_numbers', True)
  174. md_path, original_md_path = MarkdownGenerator.save_markdown(
  175. results, middle_json, doc_output_dir, doc_name,
  176. normalize_numbers=normalize_numbers
  177. )
  178. output_paths['markdown'] = str(md_path)
  179. if original_md_path:
  180. output_paths['markdown_original'] = str(original_md_path)
  181. # 5.5 保存每页独立的 Markdown
  182. if output_config.get('save_page_markdown', True):
  183. normalize_numbers = output_config.get('normalize_numbers', True)
  184. page_md_paths = MarkdownGenerator.save_page_markdowns(
  185. results, doc_output_dir, doc_name, is_pdf=is_pdf,
  186. normalize_numbers=normalize_numbers
  187. )
  188. output_paths['markdown_pages'] = page_md_paths
  189. # 6. 保存表格 HTML
  190. if output_config.get('save_html', True):
  191. html_dir = HTMLGenerator.save_table_htmls(
  192. results, doc_output_dir, doc_name, is_pdf=is_pdf
  193. )
  194. output_paths['table_htmls'] = str(html_dir)
  195. # 7. Debug 模式:保存可视化图片
  196. if output_config.get('save_layout_image', False):
  197. layout_paths = VisualizationUtils.save_layout_images(
  198. results, doc_output_dir, doc_name,
  199. draw_type_label=output_config.get('draw_type_label', True),
  200. draw_bbox_number=output_config.get('draw_bbox_number', True),
  201. is_pdf=is_pdf
  202. )
  203. output_paths['layout_images'] = layout_paths
  204. if output_config.get('save_ocr_image', False):
  205. ocr_paths = VisualizationUtils.save_ocr_images(
  206. results, doc_output_dir, doc_name, is_pdf=is_pdf
  207. )
  208. output_paths['ocr_images'] = ocr_paths
  209. logger.info(f"✅ All results saved to: {doc_output_dir}")
  210. return output_paths
  211. # ==================== 便捷函数 ====================
  212. def save_mineru_format(
  213. results: Dict[str, Any],
  214. output_dir: str,
  215. output_config: Optional[Dict[str, Any]] = None
  216. ) -> Dict[str, Any]:
  217. """
  218. 便捷函数:保存为 MinerU 格式
  219. Args:
  220. results: pipeline 处理结果
  221. output_dir: 输出目录
  222. output_config: 输出配置,支持以下选项:
  223. - create_subdir: 在输出目录下创建文档名子目录(默认 False)
  224. - save_json: 保存 middle.json
  225. - save_page_json: 保存每页 JSON
  226. - save_markdown: 保存完整 Markdown
  227. - save_page_markdown: 保存每页 Markdown
  228. - save_html: 保存表格 HTML
  229. - save_layout_image: 保存布局可视化图
  230. - save_ocr_image: 保存 OCR 可视化图
  231. - normalize_numbers: 标准化金额数字(全角→半角)
  232. Returns:
  233. 输出文件路径字典
  234. """
  235. if output_config is None:
  236. output_config = {
  237. 'create_subdir': False, # 默认不创建子目录,直接使用指定目录
  238. 'save_json': True,
  239. 'save_page_json': True,
  240. 'save_markdown': True,
  241. 'save_page_markdown': True,
  242. 'save_html': True,
  243. 'save_layout_image': False,
  244. 'save_ocr_image': False,
  245. 'normalize_numbers': True, # 默认启用数字标准化
  246. }
  247. formatter = OutputFormatterV2(output_dir)
  248. return formatter.save_results(results, output_config)