output_formatter_v2.py 9.9 KB

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