diff --git a/.gitignore b/.gitignore
index 6de528ed..d90d0c33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,5 @@ ocr_demo
/app/common/__init__.py
/magic_pdf/config/__init__.py
+source.dev.env
+
diff --git a/README_zh-CN_v2.md b/README_zh-CN_v2.md
index 7ca3ce77..94c399a6 100644
--- a/README_zh-CN_v2.md
+++ b/README_zh-CN_v2.md
@@ -21,7 +21,6 @@
-MinerU: 端到端的PDF解析工具(基于PDF-Extract-Kit)支持PDF转Markdown🚀🚀🚀
PDF-Extract-Kit: 高质量PDF解析工具箱🔥🔥🔥
@@ -82,7 +81,9 @@
# MinerU
## 项目简介
MinerU是一款将PDF转化为机器可读格式的工具(如markdown、json),可以很方便地抽取为任意格式。
-MinerU诞生于[书生-浦语](https://github.com/InternLM/InternLM)的预训练过程中,我们将会集中精力解决科技文献中的符号转化问题,以此在大模型时代为科技发展做出一点贡献。
+MinerU诞生于[书生-浦语](https://github.com/InternLM/InternLM)的预训练过程中,我们将会集中精力解决科技文献中的符号转化问题,希望在大模型时代为科技发展做出贡献。
+
+https://github.com/user-attachments/assets/4bea02c9-6d54-4cd6-97ed-dff14340982c
## 主要功能
@@ -190,7 +191,24 @@ pip install magic-pdf[full]==0.6.2b1 detectron2 --extra-index-url https://wheels
### 命令行
-TODO
+```bash
+magic-pdf -p {some_pdf} -o {some_output_dir}
+```
+
+运行完命令后输出的结果会保存在`{some_output_dir}`目录下, 输出的文件列表如下
+
+```text
+├── some_pdf.md # markdown 文件
+├── images # 存放图片目录
+├── layout.pdf # layout 绘图
+├── middle.json # minerU 中间处理结果
+├── model.json # 模型推理结果
+├── origin.pdf # 原 pdf 文件
+└── spans.pdf # 最小粒度的bbox位置信息绘图
+```
+
+更多有关输出文件的信息,请参考[输出文件说明](docs/output_file_zh_cn.md)
+
### API
@@ -266,6 +284,7 @@ The project currently leverages PyMuPDF to deliver advanced functionalities; how
# Acknowledgments
+- [StructEqTable](https://github.com/UniModal4Reasoning/StructEqTable-Deploy) 🔥🔥🔥
- [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)
- [PyMuPDF](https://github.com/pymupdf/PyMuPDF)
- [fast-langdetect](https://github.com/LlmKira/fast-langdetect)
diff --git a/docs/FAQ_zh_cn.md b/docs/FAQ_zh_cn.md
index b5932b60..ef58eea0 100644
--- a/docs/FAQ_zh_cn.md
+++ b/docs/FAQ_zh_cn.md
@@ -50,20 +50,7 @@ pip install paddlepaddle==3.0.0b1
可能是由于模型文件未下载完整导致,可尝试重现下载模型文件后再试
参考:https://github.com/opendatalab/MinerU/issues/143
-### 7.程序运行完成后,找不到tmp目录
-
-程序输出目录是在"magic-pdf.json"中通过
-```json
-{
- "temp-output-dir": "/tmp"
-}
-```
-进行配置的。
-如果没有更改这个参数,使用默认的配置执行程序,在linux/macOS会在绝对路径"/tmp"下创建一个"magic-pdf"文件夹作为输出路径。
-而在windows下,默认的输出路径与执行命令时,命令行所在的盘符相关,如果命令行在C盘,则默认输出路径为"C://tmp/magic-pdf"。
-参考:https://github.com/opendatalab/MinerU/issues/149
-
-### 8.模型文件应该下载到哪里/models-dir的配置应该怎么填
+### 7.模型文件应该下载到哪里/models-dir的配置应该怎么填
模型文件的路径输入是在"magic-pdf.json"中通过
```json
@@ -75,14 +62,14 @@ pip install paddlepaddle==3.0.0b1
这个路径是绝对路径而不是相对路径,绝对路径的获取可在models目录中通过命令 "pwd" 获取。
参考:https://github.com/opendatalab/MinerU/issues/155#issuecomment-2230216874
-### 9.命令行中 --model "model_json_path" 指的是什么?
+### 8.命令行中 --model "model_json_path" 指的是什么?
model_json 指的是通过模型分析后生成的一种有特定格式的json文件。
如果使用 https://github.com/opendatalab/PDF-Extract-Kit 项目生成,该文件一般在项目的output目录下。
如果使用 MinerU 的命令行调用内置的模型分析,该文件一般在输出路径"/tmp/magic-pdf/pdf-name"下。
参考:https://github.com/opendatalab/MinerU/issues/128
-### 10.Linux下报错:Required dependency not installed, please install by "pip install magic-pdf[full-cpu] detectron2 --extra-index-url https://myhloli.github.io/wheels/"
+### 9.Linux下报错:Required dependency not installed, please install by "pip install magic-pdf[full-cpu] detectron2 --extra-index-url https://myhloli.github.io/wheels/"
这种情况可以先使用pip list 检查一下自己的依赖库列表,重点确认下以下几个库有没有安装(版本不一定完全一致,有就可以)
```bash
diff --git a/docs/how_to_download_models_en.md b/docs/how_to_download_models_en.md
index 7ae49824..b225e643 100644
--- a/docs/how_to_download_models_en.md
+++ b/docs/how_to_download_models_en.md
@@ -35,7 +35,7 @@ model_dir = snapshot_download('wanderkid/PDF-Extract-Kit')
Alternatively, you can use Git to clone the model repository from ModelScope:
```bash
-git clone https://www.modelscope.cn/wanderkid/PDF-Extract-Kit.git
+git lfs clone https://www.modelscope.cn/wanderkid/PDF-Extract-Kit.git
```
@@ -57,4 +57,4 @@ Put [model files]() here:
│ ├── tokenizer_config.json
│ └── tokenizer.json
└── README.md
-```
\ No newline at end of file
+```
diff --git a/docs/images/layout_example.png b/docs/images/layout_example.png
new file mode 100644
index 00000000..14e2116f
Binary files /dev/null and b/docs/images/layout_example.png differ
diff --git a/docs/images/poly.png b/docs/images/poly.png
new file mode 100644
index 00000000..14af7726
Binary files /dev/null and b/docs/images/poly.png differ
diff --git a/docs/images/spans_example.png b/docs/images/spans_example.png
new file mode 100644
index 00000000..14de87ed
Binary files /dev/null and b/docs/images/spans_example.png differ
diff --git a/docs/output_file_zh_cn.md b/docs/output_file_zh_cn.md
new file mode 100644
index 00000000..031631e5
--- /dev/null
+++ b/docs/output_file_zh_cn.md
@@ -0,0 +1,327 @@
+
+
+## 概览
+`magic-pdf` 命令执行后除了输出和 markdown 有关的文件以外,还会生成若干个和 markdown 无关的文件。现在将一一介绍这些文件
+
+
+### layout.pdf
+每一页的 layout 均由一个或多个框组成。 每个框左上脚的数字表明它们的序号。此外 layout.pdf 框内用不同的背景色块圈定不同的内容块。
+
+
+
+
+### spans.pdf
+根据 span 类型的不同,采用不同颜色线框绘制页面上所有 span。该文件可以用于质检,可以快速排查出文本丢失、行间公式未识别等问题。
+
+
+
+
+### model.json
+
+#### 结构定义
+```python
+from pydantic import BaseModel, Field
+from enum import IntEnum
+
+class CategoryType(IntEnum):
+ title = 0 # 标题
+ plain_text = 1 # 文本
+ abandon = 2 # 包括页眉页脚页码和页面注释
+ figure = 3 # 图片
+ figure_caption = 4 # 图片描述
+ table = 5 # 表格
+ table_caption = 6 # 表格描述
+ table_footnote = 7 # 表格注释
+ isolate_formula = 8 # 行间公式
+ formula_caption = 9 # 行间公式的标号
+
+ embedding = 13 # 行内公式
+ isolated = 14 # 行间公式
+ text = 15 # ocr 识别结果
+
+
+class PageInfo(BaseModel):
+ page_no: int = Field(description="页码序号,第一页的序号是 0", ge=0)
+ height: int = Field(description="页面高度", gt=0)
+ width: int = Field(description="页面宽度", ge=0)
+
+class ObjectInferenceResult(BaseModel):
+ category_id: CategoryType = Field(description="类别", ge=0)
+ poly: list[float] = Field(description="四边形坐标, 分别是 左上,右上,右下,左下 四点的坐标")
+ score: float = Field(description="推理结果的置信度")
+ latex: str | None = Field(description="latex 解析结果", default=None)
+ html: str | None = Field(description="html 解析结果", default=None)
+
+class PageInferenceResults(BaseModel):
+ layout_dets: list[ObjectInferenceResult] = Field(description="页面识别结果", ge=0)
+ page_info: PageInfo = Field(description="页面元信息")
+
+
+# 所有页面的推理结果按照页码顺序依次放到列表中即为 minerU 推理结果
+inference_result: list[PageInferenceResults] = []
+
+```
+
+poly 坐标的格式 [x0, y0, x1, y1, x2, y2, x3, y3], 分别表示左上、右上、右下、左小四点的坐标
+
+
+
+#### 示例数据
+
+```json
+[
+ {
+ "layout_dets": [
+ {
+ "category_id": 2,
+ "poly": [
+ 99.1906967163086,
+ 100.3119125366211,
+ 730.3707885742188,
+ 100.3119125366211,
+ 730.3707885742188,
+ 245.81326293945312,
+ 99.1906967163086,
+ 245.81326293945312
+ ],
+ "score": 0.9999997615814209
+ }
+ ],
+ "page_info": {
+ "page_no": 0,
+ "height": 2339,
+ "width": 1654
+ }
+ },
+ {
+ "layout_dets": [
+ {
+ "category_id": 5,
+ "poly": [
+ 99.13092803955078,
+ 2210.680419921875,
+ 497.3183898925781,
+ 2210.680419921875,
+ 497.3183898925781,
+ 2264.78076171875,
+ 99.13092803955078,
+ 2264.78076171875
+ ],
+ "score": 0.9999997019767761
+ }
+ ],
+ "page_info": {
+ "page_no": 1,
+ "height": 2339,
+ "width": 1654
+ }
+ }
+]
+```
+
+
+### middle.json
+
+| 字段名 | 解释 |
+| :-----| :---- |
+|pdf_info |list,每个元素都是一个dict,这个dict是每一页pdf的解析结果,详见下表 |
+|_parse_type | ocr \| txt,用来标识本次解析的中间态使用的模式 |
+
+
+
+**pdf_info**
+字段结构说明
+
+| 字段名 | 解释 |
+| :-----| :---- |
+| preproc_blocks | pdf预处理后,未分段的中间结果 |
+| layout_bboxes | 布局分割的结果,含有布局的方向(垂直、水平),和bbox,按阅读顺序排序 |
+| page_idx | 页码,从0开始 |
+| page_size | 页面的宽度和高度 |
+| _layout_tree | 布局树状结构 |
+| images | list,每个元素是一个dict,每个dict表示一个img_block |
+| tables | list,每个元素是一个dict,每个dict表示一个table_block |
+| interline_equations | list,每个元素是一个dict,每个dict表示一个interline_equation_block |
+| discarded_blocks | List, 模型返回的需要drop的block信息 |
+| para_blocks | 将preproc_blocks进行分段之后的结果 |
+
+上表中 `para_blocks` 是个dict的数组,每个dict是一个block结构,block最多支持一次嵌套
+
+
+
+**block**
+
+外层block被称为一级block,一级block中的字段包括
+
+| type | block类型(table\|image)|
+| :-----| :---- |
+|bbox | block矩形框坐标 |
+|blocks |list,里面的每个元素都是一个dict格式的二级block |
+
+
+一级block只有"table"和"image"两种类型,其余block均为二级block
+
+二级block中的字段包括
+
+| type | desc |
+| :-----| :---- |
+| bbox | block矩形框坐标 |
+| lines | list,每个元素都是一个dict表示的line,用来描述一行信息的构成|
+
+二级block的类型详解
+| type | desc |
+| :-----| :---- |
+| image_body | 图像的本体 |
+| image_caption | 图像的描述文本 |
+| table_body | 表格本体 |
+| table_caption | 表格的描述文本 |
+| table_footnote | 表格的脚注 |
+| text | 文本块 |
+| title | 标题块 |
+| interline_equation | 行间公式块|
+
+
+
+**line**
+
+line 的 字段格式如下
+| 字段名 | 解释 |
+| :-----| :---- |
+| bbox | line的矩形框坐标 |
+| spans | list,每个元素都是一个dict表示的span,用来描述一个最小组成单元的构成 |
+
+
+
+
+**span**
+
+| 字段名 | 解释 |
+| :-----| :---- |
+| bbox | span的矩形框坐标 |
+| type | span的类型 |
+| content \| img_path | 文本类型的span使用content,图表类使用img_path 用来存储实际的文本或者截图路径信息 |
+
+span 的类型有如下几种
+
+| type | desc |
+| :-----| :---- |
+| image | 图片 |
+| table | 表格 |
+| text | 文本 |
+| inline_equation | 行内公式 |
+| interline_equation | 行间公式 |
+
+
+**总结**
+
+span是所有元素的最小存储单元
+
+para_blocks内存储的元素为区块信息
+
+区块结构为
+
+一级block(如有)->二级block->line->span
+
+
+#### 示例数据
+
+```json
+{
+ "pdf_info": [
+ {
+ "preproc_blocks": [
+ {
+ "type": "text",
+ "bbox": [
+ 52,
+ 61.956024169921875,
+ 294,
+ 82.99800872802734
+ ],
+ "lines": [
+ {
+ "bbox": [
+ 52,
+ 61.956024169921875,
+ 294,
+ 72.0000228881836
+ ],
+ "spans": [
+ {
+ "bbox": [
+ 54.0,
+ 61.956024169921875,
+ 296.2261657714844,
+ 72.0000228881836
+ ],
+ "content": "dependent on the service headway and the reliability of the departure ",
+ "type": "text",
+ "score": 1.0
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "layout_bboxes": [
+ {
+ "layout_bbox": [
+ 52,
+ 61,
+ 294,
+ 731
+ ],
+ "layout_label": "V",
+ "sub_layout": []
+ }
+ ],
+ "page_idx": 0,
+ "page_size": [
+ 612.0,
+ 792.0
+ ],
+ "_layout_tree": [],
+ "images": [],
+ "tables": [],
+ "interline_equations": [],
+ "discarded_blocks": [],
+ "para_blocks": [
+ {
+ "type": "text",
+ "bbox": [
+ 52,
+ 61.956024169921875,
+ 294,
+ 82.99800872802734
+ ],
+ "lines": [
+ {
+ "bbox": [
+ 52,
+ 61.956024169921875,
+ 294,
+ 72.0000228881836
+ ],
+ "spans": [
+ {
+ "bbox": [
+ 54.0,
+ 61.956024169921875,
+ 296.2261657714844,
+ 72.0000228881836
+ ],
+ "content": "dependent on the service headway and the reliability of the departure ",
+ "type": "text",
+ "score": 1.0
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "_parse_type": "txt",
+ "_version_name": "0.6.1"
+}
+```
\ No newline at end of file
diff --git a/magic-pdf.template.json b/magic-pdf.template.json
index 2c0223db..388682ca 100644
--- a/magic-pdf.template.json
+++ b/magic-pdf.template.json
@@ -3,7 +3,6 @@
"bucket-name-1":["ak", "sk", "endpoint"],
"bucket-name-2":["ak", "sk", "endpoint"]
},
- "temp-output-dir":"/tmp",
"models-dir":"/tmp/models",
"device-mode":"cpu"
}
\ No newline at end of file
diff --git a/magic_pdf/libs/config_reader.py b/magic_pdf/libs/config_reader.py
index 7454c577..9ebc781f 100644
--- a/magic_pdf/libs/config_reader.py
+++ b/magic_pdf/libs/config_reader.py
@@ -57,16 +57,6 @@ def get_bucket_name(path):
return bucket
-def get_local_dir():
- config = read_config()
- local_dir = config.get("temp-output-dir")
- if local_dir is None:
- logger.warning(f"'temp-output-dir' not found in {CONFIG_FILE_NAME}, use '/tmp' as default")
- return "/tmp"
- else:
- return local_dir
-
-
def get_local_models_dir():
config = read_config()
models_dir = config.get("models-dir")
diff --git a/magic_pdf/rw/AbsReaderWriter.py b/magic_pdf/rw/AbsReaderWriter.py
index ce5e0774..78e455af 100644
--- a/magic_pdf/rw/AbsReaderWriter.py
+++ b/magic_pdf/rw/AbsReaderWriter.py
@@ -2,33 +2,16 @@ from abc import ABC, abstractmethod
class AbsReaderWriter(ABC):
- """
- 同时支持二进制和文本读写的抽象类
- """
MODE_TXT = "text"
MODE_BIN = "binary"
-
- def __init__(self, parent_path):
- # 初始化代码可以在这里添加,如果需要的话
- self.parent_path = parent_path # 对于本地目录是父目录,对于s3是会写到这个path下。
-
@abstractmethod
def read(self, path: str, mode=MODE_TXT):
- """
- 无论对于本地还是s3的路径,检查如果path是绝对路径,那么就不再 拼接parent_path, 如果是相对路径就拼接parent_path
- """
raise NotImplementedError
@abstractmethod
def write(self, content: str, path: str, mode=MODE_TXT):
- """
- 无论对于本地还是s3的路径,检查如果path是绝对路径,那么就不再 拼接parent_path, 如果是相对路径就拼接parent_path
- """
raise NotImplementedError
@abstractmethod
- def read_jsonl(self, path: str, byte_start=0, byte_end=None, encoding='utf-8'):
- """
- 无论对于本地还是s3的路径,检查如果path是绝对路径,那么就不再 拼接parent_path, 如果是相对路径就拼接parent_path
- """
+ def read_offset(self, path: str, offset=0, limit=None) -> bytes:
raise NotImplementedError
diff --git a/magic_pdf/rw/DiskReaderWriter.py b/magic_pdf/rw/DiskReaderWriter.py
index 4e034c80..dd627fde 100644
--- a/magic_pdf/rw/DiskReaderWriter.py
+++ b/magic_pdf/rw/DiskReaderWriter.py
@@ -3,34 +3,29 @@ from magic_pdf.rw.AbsReaderWriter import AbsReaderWriter
from loguru import logger
-MODE_TXT = "text"
-MODE_BIN = "binary"
-
-
class DiskReaderWriter(AbsReaderWriter):
-
def __init__(self, parent_path, encoding="utf-8"):
self.path = parent_path
self.encoding = encoding
- def read(self, path, mode=MODE_TXT):
+ def read(self, path, mode=AbsReaderWriter.MODE_TXT):
if os.path.isabs(path):
abspath = path
else:
abspath = os.path.join(self.path, path)
if not os.path.exists(abspath):
- logger.error(f"文件 {abspath} 不存在")
- raise Exception(f"文件 {abspath} 不存在")
- if mode == MODE_TXT:
+ logger.error(f"file {abspath} not exists")
+ raise Exception(f"file {abspath} no exists")
+ if mode == AbsReaderWriter.MODE_TXT:
with open(abspath, "r", encoding=self.encoding) as f:
return f.read()
- elif mode == MODE_BIN:
+ elif mode == AbsReaderWriter.MODE_BIN:
with open(abspath, "rb") as f:
return f.read()
else:
raise ValueError("Invalid mode. Use 'text' or 'binary'.")
- def write(self, content, path, mode=MODE_TXT):
+ def write(self, content, path, mode=AbsReaderWriter.MODE_TXT):
if os.path.isabs(path):
abspath = path
else:
@@ -38,29 +33,42 @@ class DiskReaderWriter(AbsReaderWriter):
directory_path = os.path.dirname(abspath)
if not os.path.exists(directory_path):
os.makedirs(directory_path)
- if mode == MODE_TXT:
+ if mode == AbsReaderWriter.MODE_TXT:
with open(abspath, "w", encoding=self.encoding, errors="replace") as f:
f.write(content)
- elif mode == MODE_BIN:
+ elif mode == AbsReaderWriter.MODE_BIN:
with open(abspath, "wb") as f:
f.write(content)
else:
raise ValueError("Invalid mode. Use 'text' or 'binary'.")
- def read_jsonl(self, path: str, byte_start=0, byte_end=None, encoding="utf-8"):
- return self.read(path)
+ def read_offset(self, path: str, offset=0, limit=None):
+ abspath = path
+ if not os.path.isabs(path):
+ abspath = os.path.join(self.path, path)
+ with open(abspath, "rb") as f:
+ f.seek(offset)
+ return f.read(limit)
-# 使用示例
if __name__ == "__main__":
- file_path = "io/test/example.txt"
- drw = DiskReaderWriter("D:\projects\papayfork\Magic-PDF\magic_pdf")
+ if 0:
+ file_path = "io/test/example.txt"
+ drw = DiskReaderWriter("D:\projects\papayfork\Magic-PDF\magic_pdf")
- # 写入内容到文件
- drw.write(b"Hello, World!", path="io/test/example.txt", mode="binary")
+ # 写入内容到文件
+ drw.write(b"Hello, World!", path="io/test/example.txt", mode="binary")
+
+ # 从文件读取内容
+ content = drw.read(path=file_path)
+ if content:
+ logger.info(f"从 {file_path} 读取的内容: {content}")
+ if 1:
+ drw = DiskReaderWriter("/opt/data/pdf/resources/test/io/")
+ content_bin = drw.read_offset("1.txt")
+ assert content_bin == b"ABCD!"
+
+ content_bin = drw.read_offset("1.txt", offset=1, limit=2)
+ assert content_bin == b"BC"
- # 从文件读取内容
- content = drw.read(path=file_path)
- if content:
- logger.info(f"从 {file_path} 读取的内容: {content}")
diff --git a/magic_pdf/rw/S3ReaderWriter.py b/magic_pdf/rw/S3ReaderWriter.py
index df394a1e..ac1cc1f1 100644
--- a/magic_pdf/rw/S3ReaderWriter.py
+++ b/magic_pdf/rw/S3ReaderWriter.py
@@ -2,16 +2,18 @@ from magic_pdf.rw.AbsReaderWriter import AbsReaderWriter
from magic_pdf.libs.commons import parse_aws_param, parse_bucket_key, join_path
import boto3
from loguru import logger
-from boto3.s3.transfer import TransferConfig
from botocore.config import Config
-import os
-
-MODE_TXT = "text"
-MODE_BIN = "binary"
class S3ReaderWriter(AbsReaderWriter):
- def __init__(self, ak: str, sk: str, endpoint_url: str, addressing_style: str = 'auto', parent_path: str = ''):
+ def __init__(
+ self,
+ ak: str,
+ sk: str,
+ endpoint_url: str,
+ addressing_style: str = "auto",
+ parent_path: str = "",
+ ):
self.client = self._get_client(ak, sk, endpoint_url, addressing_style)
self.path = parent_path
@@ -21,12 +23,14 @@ class S3ReaderWriter(AbsReaderWriter):
aws_access_key_id=ak,
aws_secret_access_key=sk,
endpoint_url=endpoint_url,
- config=Config(s3={"addressing_style": addressing_style},
- retries={'max_attempts': 5, 'mode': 'standard'}),
+ config=Config(
+ s3={"addressing_style": addressing_style},
+ retries={"max_attempts": 5, "mode": "standard"},
+ ),
)
return s3_client
- def read(self, s3_relative_path, mode=MODE_TXT, encoding="utf-8"):
+ def read(self, s3_relative_path, mode=AbsReaderWriter.MODE_TXT, encoding="utf-8"):
if s3_relative_path.startswith("s3://"):
s3_path = s3_relative_path
else:
@@ -34,22 +38,22 @@ class S3ReaderWriter(AbsReaderWriter):
bucket_name, key = parse_bucket_key(s3_path)
res = self.client.get_object(Bucket=bucket_name, Key=key)
body = res["Body"].read()
- if mode == MODE_TXT:
+ if mode == AbsReaderWriter.MODE_TXT:
data = body.decode(encoding) # Decode bytes to text
- elif mode == MODE_BIN:
+ elif mode == AbsReaderWriter.MODE_BIN:
data = body
else:
raise ValueError("Invalid mode. Use 'text' or 'binary'.")
return data
- def write(self, content, s3_relative_path, mode=MODE_TXT, encoding="utf-8"):
+ def write(self, content, s3_relative_path, mode=AbsReaderWriter.MODE_TXT, encoding="utf-8"):
if s3_relative_path.startswith("s3://"):
s3_path = s3_relative_path
else:
s3_path = join_path(self.path, s3_relative_path)
- if mode == MODE_TXT:
+ if mode == AbsReaderWriter.MODE_TXT:
body = content.encode(encoding) # Encode text data as bytes
- elif mode == MODE_BIN:
+ elif mode == AbsReaderWriter.MODE_BIN:
body = content
else:
raise ValueError("Invalid mode. Use 'text' or 'binary'.")
@@ -57,51 +61,82 @@ class S3ReaderWriter(AbsReaderWriter):
self.client.put_object(Body=body, Bucket=bucket_name, Key=key)
logger.info(f"内容已写入 {s3_path} ")
- def read_jsonl(self, path: str, byte_start=0, byte_end=None, mode=MODE_TXT, encoding='utf-8'):
+ def read_offset(self, path: str, offset=0, limit=None) -> bytes:
if path.startswith("s3://"):
s3_path = path
else:
s3_path = join_path(self.path, path)
bucket_name, key = parse_bucket_key(s3_path)
- range_header = f'bytes={byte_start}-{byte_end}' if byte_end else f'bytes={byte_start}-'
+ range_header = (
+ f"bytes={offset}-{offset+limit-1}" if limit else f"bytes={offset}-"
+ )
res = self.client.get_object(Bucket=bucket_name, Key=key, Range=range_header)
- body = res["Body"].read()
- if mode == MODE_TXT:
- data = body.decode(encoding) # Decode bytes to text
- elif mode == MODE_BIN:
- data = body
- else:
- raise ValueError("Invalid mode. Use 'text' or 'binary'.")
- return data
+ return res["Body"].read()
if __name__ == "__main__":
- # Config the connection info
- ak = ""
- sk = ""
- endpoint_url = ""
- addressing_style = "auto"
- bucket_name = ""
- # Create an S3ReaderWriter object
- s3_reader_writer = S3ReaderWriter(ak, sk, endpoint_url, addressing_style, "s3://bucket_name/")
+ if 0:
+ # Config the connection info
+ ak = ""
+ sk = ""
+ endpoint_url = ""
+ addressing_style = "auto"
+ bucket_name = ""
+ # Create an S3ReaderWriter object
+ s3_reader_writer = S3ReaderWriter(
+ ak, sk, endpoint_url, addressing_style, "s3://bucket_name/"
+ )
- # Write text data to S3
- text_data = "This is some text data"
- s3_reader_writer.write(data=text_data, s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=MODE_TXT)
+ # Write text data to S3
+ text_data = "This is some text data"
+ s3_reader_writer.write(
+ text_data,
+ s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json",
+ mode=AbsReaderWriter.MODE_TXT,
+ )
- # Read text data from S3
- text_data_read = s3_reader_writer.read(s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=MODE_TXT)
- logger.info(f"Read text data from S3: {text_data_read}")
- # Write binary data to S3
- binary_data = b"This is some binary data"
- s3_reader_writer.write(data=text_data, s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=MODE_BIN)
+ # Read text data from S3
+ text_data_read = s3_reader_writer.read(
+ s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=AbsReaderWriter.MODE_TXT
+ )
+ logger.info(f"Read text data from S3: {text_data_read}")
+ # Write binary data to S3
+ binary_data = b"This is some binary data"
+ s3_reader_writer.write(
+ text_data,
+ s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json",
+ mode=AbsReaderWriter.MODE_BIN,
+ )
- # Read binary data from S3
- binary_data_read = s3_reader_writer.read(s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=MODE_BIN)
- logger.info(f"Read binary data from S3: {binary_data_read}")
+ # Read binary data from S3
+ binary_data_read = s3_reader_writer.read(
+ s3_relative_path=f"s3://{bucket_name}/ebook/test/test.json", mode=AbsReaderWriter.MODE_BIN
+ )
+ logger.info(f"Read binary data from S3: {binary_data_read}")
- # Range Read text data from S3
- binary_data_read = s3_reader_writer.read_jsonl(path=f"s3://{bucket_name}/ebook/test/test.json",
- byte_start=0, byte_end=10, mode=MODE_BIN)
- logger.info(f"Read binary data from S3: {binary_data_read}")
+ # Range Read text data from S3
+ binary_data_read = s3_reader_writer.read_offset(
+ path=f"s3://{bucket_name}/ebook/test/test.json", offset=0, limit=10
+ )
+ logger.info(f"Read binary data from S3: {binary_data_read}")
+ if 1:
+ import os
+ import json
+
+ ak = os.getenv("AK", "")
+ sk = os.getenv("SK", "")
+ endpoint_url = os.getenv("ENDPOINT", "")
+ bucket = os.getenv("S3_BUCKET", "")
+ prefix = os.getenv("S3_PREFIX", "")
+ key_basename = os.getenv("S3_KEY_BASENAME", "")
+ s3_reader_writer = S3ReaderWriter(
+ ak, sk, endpoint_url, "auto", f"s3://{bucket}/{prefix}"
+ )
+ content_bin = s3_reader_writer.read_offset(key_basename)
+ assert content_bin[:10] == b'{"track_id'
+ assert content_bin[-10:] == b'r":null}}\n'
+
+ content_bin = s3_reader_writer.read_offset(key_basename, offset=424, limit=426)
+ jso = json.dumps(content_bin.decode("utf-8"))
+ print(jso)
diff --git a/magic_pdf/cli/__init__.py b/magic_pdf/tools/__init__.py
similarity index 100%
rename from magic_pdf/cli/__init__.py
rename to magic_pdf/tools/__init__.py
diff --git a/magic_pdf/tools/cli.py b/magic_pdf/tools/cli.py
new file mode 100644
index 00000000..49b29442
--- /dev/null
+++ b/magic_pdf/tools/cli.py
@@ -0,0 +1,75 @@
+import os
+import click
+from loguru import logger
+from pathlib import Path
+from magic_pdf.rw.DiskReaderWriter import DiskReaderWriter
+from magic_pdf.rw.AbsReaderWriter import AbsReaderWriter
+import magic_pdf.model as model_config
+from magic_pdf.tools.common import parse_pdf_methods, do_parse
+
+
+@click.command()
+@click.option(
+ "-p",
+ "--path",
+ "path",
+ type=click.Path(exists=True),
+ required=True,
+ help="local pdf filepath or directory",
+)
+@click.option(
+ "-o",
+ "--output-dir",
+ "output_dir",
+ type=str,
+ help="output local directory",
+ default="",
+)
+@click.option(
+ "-m",
+ "--method",
+ "method",
+ type=parse_pdf_methods,
+ help="""the method for parsing pdf.
+ocr: using ocr technique to extract information from pdf.
+txt: suitable for the text-based pdf only and outperform ocr.
+auto: automatically choose the best method for parsing pdf from ocr and txt""",
+ default="auto",
+)
+def cli(path, output_dir, method):
+ model_config.__use_inside_model__ = True
+ model_config.__model_mode__ = "full"
+ if output_dir == "":
+ if os.path.isdir(path):
+ output_dir = os.path.join(path, "output")
+ else:
+ output_dir = os.path.join(os.path.dirname(path), "output")
+
+ def read_fn(path):
+ disk_rw = DiskReaderWriter(os.path.dirname(path))
+ return disk_rw.read(os.path.basename(path), AbsReaderWriter.MODE_BIN)
+
+ def parse_doc(doc_path: str):
+ try:
+ file_name = str(Path(doc_path).stem)
+ pdf_data = read_fn(doc_path)
+ do_parse(
+ output_dir,
+ file_name,
+ pdf_data,
+ [],
+ method,
+ )
+
+ except Exception as e:
+ logger.exception(e)
+
+ if os.path.isdir(path):
+ for doc_path in Path(path).glob("*.pdf"):
+ parse_doc(doc_path)
+ else:
+ parse_doc(path)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/magic_pdf/tools/cli_dev.py b/magic_pdf/tools/cli_dev.py
new file mode 100644
index 00000000..01b33cef
--- /dev/null
+++ b/magic_pdf/tools/cli_dev.py
@@ -0,0 +1,154 @@
+import os
+import json as json_parse
+import click
+from pathlib import Path
+from magic_pdf.libs.path_utils import (
+ parse_s3path,
+ parse_s3_range_params,
+ remove_non_official_s3_args,
+)
+from magic_pdf.libs.config_reader import (
+ get_s3_config,
+)
+from magic_pdf.rw.S3ReaderWriter import S3ReaderWriter
+from magic_pdf.rw.DiskReaderWriter import DiskReaderWriter
+from magic_pdf.rw.AbsReaderWriter import AbsReaderWriter
+import magic_pdf.model as model_config
+from magic_pdf.tools.common import parse_pdf_methods, do_parse
+
+
+def read_s3_path(s3path):
+ bucket, key = parse_s3path(s3path)
+
+ s3_ak, s3_sk, s3_endpoint = get_s3_config(bucket)
+ s3_rw = S3ReaderWriter(
+ s3_ak, s3_sk, s3_endpoint, "auto", remove_non_official_s3_args(s3path)
+ )
+ may_range_params = parse_s3_range_params(s3path)
+ if may_range_params is None or 2 != len(may_range_params):
+ byte_start, byte_end = 0, None
+ else:
+ byte_start, byte_end = int(may_range_params[0]), int(may_range_params[1])
+ byte_end += byte_start - 1
+ return s3_rw.read_jsonl(
+ remove_non_official_s3_args(s3path),
+ byte_start,
+ byte_end,
+ AbsReaderWriter.MODE_BIN,
+ )
+
+
+@click.group()
+def cli():
+ pass
+
+
+@cli.command()
+@click.option(
+ "-j",
+ "--jsonl",
+ "jsonl",
+ type=str,
+ help="输入 jsonl 路径,本地或者 s3 上的文件",
+ required=True,
+)
+@click.option(
+ "-m",
+ "--method",
+ "method",
+ type=parse_pdf_methods,
+ help="指定解析方法。txt: 文本型 pdf 解析方法, ocr: 光学识别解析 pdf, auto: 程序智能选择解析方法",
+ default="auto",
+)
+@click.option(
+ "-o",
+ "--output-dir",
+ "output_dir",
+ type=str,
+ help="输出到本地目录",
+ default="",
+)
+def jsonl(jsonl, method, output_dir):
+ print("haha")
+ model_config.__use_inside_model__ = False
+ full_jsonl_path = os.path.realpath(jsonl)
+ if output_dir == "":
+ output_dir = os.path.join(os.path.dirname(full_jsonl_path), "output")
+
+ if jsonl.startswith("s3://"):
+ jso = json_parse.loads(read_s3_path(jsonl).decode("utf-8"))
+ else:
+ with open(jsonl) as f:
+ jso = json_parse.loads(f.readline())
+ s3_file_path = jso.get("file_location")
+ if s3_file_path is None:
+ s3_file_path = jso.get("path")
+ pdf_file_name = Path(s3_file_path).stem
+ pdf_data = read_s3_path(s3_file_path)
+
+
+ print(pdf_file_name, jso, method)
+ do_parse(
+ output_dir,
+ pdf_file_name,
+ pdf_data,
+ jso["doc_layout_result"],
+ method,
+ f_dump_content_list=True,
+ )
+
+
+@cli.command()
+@click.option(
+ "-p",
+ "--pdf",
+ "pdf",
+ type=click.Path(exists=True),
+ required=True,
+ help="本地 PDF 文件",
+)
+@click.option(
+ "-j",
+ "--json",
+ "json_data",
+ type=click.Path(exists=True),
+ required=True,
+ help="本地模型推理出的 json 数据",
+)
+@click.option(
+ "-o", "--output-dir", "output_dir", type=str, help="本地输出目录", default=""
+)
+@click.option(
+ "-m",
+ "--method",
+ "method",
+ type=parse_pdf_methods,
+ help="指定解析方法。txt: 文本型 pdf 解析方法, ocr: 光学识别解析 pdf, auto: 程序智能选择解析方法",
+ default="auto",
+)
+def pdf(pdf, json_data, output_dir, method):
+ model_config.__use_inside_model__ = False
+ full_pdf_path = os.path.realpath(pdf)
+ if output_dir == "":
+ output_dir = os.path.join(os.path.dirname(full_pdf_path), "output")
+
+ def read_fn(path):
+ disk_rw = DiskReaderWriter(os.path.dirname(path))
+ return disk_rw.read(os.path.basename(path), AbsReaderWriter.MODE_BIN)
+
+ model_json_list = json_parse.loads(read_fn(json_data).decode("utf-8"))
+
+ file_name = str(Path(full_pdf_path).stem)
+ pdf_data = read_fn(full_pdf_path)
+ do_parse(
+ output_dir,
+ file_name,
+ pdf_data,
+ model_json_list,
+ method,
+ f_dump_content_list=True,
+ )
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/magic_pdf/tools/common.py b/magic_pdf/tools/common.py
new file mode 100644
index 00000000..70451082
--- /dev/null
+++ b/magic_pdf/tools/common.py
@@ -0,0 +1,117 @@
+import os
+import json as json_parse
+import copy
+import click
+from loguru import logger
+from magic_pdf.libs.MakeContentConfig import DropMode, MakeMode
+from magic_pdf.libs.draw_bbox import draw_layout_bbox, draw_span_bbox
+from magic_pdf.pipe.UNIPipe import UNIPipe
+from magic_pdf.pipe.OCRPipe import OCRPipe
+from magic_pdf.pipe.TXTPipe import TXTPipe
+from magic_pdf.rw.DiskReaderWriter import DiskReaderWriter
+from magic_pdf.rw.AbsReaderWriter import AbsReaderWriter
+import magic_pdf.model as model_config
+
+
+def prepare_env(output_dir, pdf_file_name, method):
+ local_parent_dir = os.path.join(output_dir, pdf_file_name, method)
+
+ local_image_dir = os.path.join(str(local_parent_dir), "images")
+ local_md_dir = local_parent_dir
+ os.makedirs(local_image_dir, exist_ok=True)
+ os.makedirs(local_md_dir, exist_ok=True)
+ return local_image_dir, local_md_dir
+
+
+def do_parse(
+ output_dir,
+ pdf_file_name,
+ pdf_bytes,
+ model_list,
+ parse_method,
+ f_draw_span_bbox=True,
+ f_draw_layout_bbox=True,
+ f_dump_md=True,
+ f_dump_middle_json=True,
+ f_dump_model_json=True,
+ f_dump_orig_pdf=True,
+ f_dump_content_list=False,
+ f_make_md_mode=MakeMode.MM_MD,
+):
+ orig_model_list = copy.deepcopy(model_list)
+ local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name, parse_method)
+ logger.info(f"local output dir is {local_md_dir}")
+ image_writer, md_writer = DiskReaderWriter(local_image_dir), DiskReaderWriter(
+ local_md_dir
+ )
+ image_dir = str(os.path.basename(local_image_dir))
+
+ if parse_method == "auto":
+ jso_useful_key = {"_pdf_type": "", "model_list": model_list}
+ pipe = UNIPipe(pdf_bytes, jso_useful_key, image_writer, is_debug=True)
+ elif parse_method == "txt":
+ pipe = TXTPipe(pdf_bytes, model_list, image_writer, is_debug=True)
+ elif parse_method == "ocr":
+ pipe = OCRPipe(pdf_bytes, model_list, image_writer, is_debug=True)
+ else:
+ logger.error("unknown parse method")
+ exit(1)
+
+ pipe.pipe_classify()
+
+ if len(model_list) == 0:
+ if model_config.__use_inside_model__:
+ pipe.pipe_analyze()
+ orig_model_list = copy.deepcopy(pipe.model_list)
+ else:
+ logger.error("need model list input")
+ exit(2)
+
+ pipe.pipe_parse()
+ pdf_info = pipe.pdf_mid_data["pdf_info"]
+ if f_draw_layout_bbox:
+ draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir)
+ if f_draw_span_bbox:
+ draw_span_bbox(pdf_info, pdf_bytes, local_md_dir)
+
+ md_content = pipe.pipe_mk_markdown(
+ image_dir, drop_mode=DropMode.NONE, md_make_mode=f_make_md_mode
+ )
+ if f_dump_md:
+ md_writer.write(
+ content=md_content,
+ path=f"{pdf_file_name}.md",
+ mode=AbsReaderWriter.MODE_TXT,
+ )
+
+ if f_dump_middle_json:
+ md_writer.write(
+ content=json_parse.dumps(pipe.pdf_mid_data, ensure_ascii=False, indent=4),
+ path="middle.json",
+ mode=AbsReaderWriter.MODE_TXT,
+ )
+
+ if f_dump_model_json:
+ md_writer.write(
+ content=json_parse.dumps(orig_model_list, ensure_ascii=False, indent=4),
+ path="model.json",
+ mode=AbsReaderWriter.MODE_TXT,
+ )
+
+ if f_dump_orig_pdf:
+ md_writer.write(
+ content=pdf_bytes,
+ path="origin.pdf",
+ mode=AbsReaderWriter.MODE_BIN,
+ )
+
+ content_list = pipe.pipe_mk_uni_format(image_dir, drop_mode=DropMode.NONE)
+ if f_dump_content_list:
+ md_writer.write(
+ content=json_parse.dumps(content_list, ensure_ascii=False, indent=4),
+ path="content_list.json",
+ mode=AbsReaderWriter.MODE_TXT,
+ )
+
+
+parse_pdf_methods = click.Choice(["ocr", "txt", "auto"])
diff --git a/setup.py b/setup.py
index 1081b908..77981d36 100644
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,8 @@ if __name__ == '__main__':
python_requires=">=3.9", # 项目依赖的 Python 版本
entry_points={
"console_scripts": [
- "magic-pdf = magic_pdf.cli.magicpdf:cli"
+ "magic-pdf = magic_pdf.tools.cli:cli",
+ "magic-pdf-dev = magic_pdf.tools.cli_dev:cli"
],
}, # 项目提供的可执行命令
include_package_data=True, # 是否包含非代码文件,如数据文件、配置文件等
diff --git a/tests/test_tools/__init__.py b/tests/test_tools/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_tools/assets/cli/path/cli_test_01.pdf b/tests/test_tools/assets/cli/path/cli_test_01.pdf
new file mode 100644
index 00000000..229be9ce
Binary files /dev/null and b/tests/test_tools/assets/cli/path/cli_test_01.pdf differ
diff --git a/tests/test_tools/assets/cli/path/cli_test_02.pdf b/tests/test_tools/assets/cli/path/cli_test_02.pdf
new file mode 100644
index 00000000..1adcc01c
Binary files /dev/null and b/tests/test_tools/assets/cli/path/cli_test_02.pdf differ
diff --git a/tests/test_tools/assets/cli/pdf/cli_test_01.pdf b/tests/test_tools/assets/cli/pdf/cli_test_01.pdf
new file mode 100644
index 00000000..229be9ce
Binary files /dev/null and b/tests/test_tools/assets/cli/pdf/cli_test_01.pdf differ
diff --git a/tests/test_tools/assets/cli_dev/cli_test_01.jsonl b/tests/test_tools/assets/cli_dev/cli_test_01.jsonl
new file mode 100644
index 00000000..c9ca39c7
--- /dev/null
+++ b/tests/test_tools/assets/cli_dev/cli_test_01.jsonl
@@ -0,0 +1 @@
+{"file_location":"tests/test_tools/assets/cli_dev/cli_test_01.pdf","doc_layout_result":[{"layout_dets":[{"category_id":1,"poly":[882.4013061523438,169.93817138671875,1552.350341796875,169.93817138671875,1552.350341796875,625.8263549804688,882.4013061523438,625.8263549804688],"score":0.999992311000824},{"category_id":1,"poly":[882.474853515625,1450.92822265625,1551.4490966796875,1450.92822265625,1551.4490966796875,1877.5712890625,882.474853515625,1877.5712890625],"score":0.9999903440475464},{"category_id":1,"poly":[881.6513061523438,626.2058715820312,1552.1400146484375,626.2058715820312,1552.1400146484375,1450.604736328125,881.6513061523438,1450.604736328125],"score":0.9999856352806091},{"category_id":1,"poly":[149.41075134277344,232.1595001220703,819.0465087890625,232.1595001220703,819.0465087890625,625.8865356445312,149.41075134277344,625.8865356445312],"score":0.99998539686203},{"category_id":1,"poly":[149.3945770263672,1215.5172119140625,817.8850708007812,1215.5172119140625,817.8850708007812,1304.873291015625,149.3945770263672,1304.873291015625],"score":0.9999765157699585},{"category_id":1,"poly":[882.6979370117188,1880.13916015625,1552.15185546875,1880.13916015625,1552.15185546875,2031.339599609375,882.6979370117188,2031.339599609375],"score":0.9999744892120361},{"category_id":1,"poly":[148.96054077148438,743.3055419921875,818.6231689453125,743.3055419921875,818.6231689453125,1074.2369384765625,148.96054077148438,1074.2369384765625],"score":0.9999669790267944},{"category_id":1,"poly":[148.8435516357422,1791.14306640625,818.6885375976562,1791.14306640625,818.6885375976562,2030.794189453125,148.8435516357422,2030.794189453125],"score":0.9999618530273438},{"category_id":0,"poly":[150.7009735107422,684.0087890625,623.5106201171875,684.0087890625,623.5106201171875,717.03662109375,150.7009735107422,717.03662109375],"score":0.9999415278434753},{"category_id":8,"poly":[146.48068237304688,1331.6737060546875,317.2640075683594,1331.6737060546875,317.2640075683594,1400.1722412109375,146.48068237304688,1400.1722412109375],"score":0.9998958110809326},{"category_id":1,"poly":[149.42420959472656,1430.8782958984375,818.9042358398438,1430.8782958984375,818.9042358398438,1672.7386474609375,149.42420959472656,1672.7386474609375],"score":0.9998599290847778},{"category_id":1,"poly":[149.18746948242188,172.10252380371094,818.5662231445312,172.10252380371094,818.5662231445312,230.4594268798828,149.18746948242188,230.4594268798828],"score":0.9997718334197998},{"category_id":0,"poly":[149.0175018310547,1732.1090087890625,702.1005859375,1732.1090087890625,702.1005859375,1763.6046142578125,149.0175018310547,1763.6046142578125],"score":0.9997085928916931},{"category_id":2,"poly":[1519.802490234375,98.59099578857422,1551.985107421875,98.59099578857422,1551.985107421875,119.48420715332031,1519.802490234375,119.48420715332031],"score":0.9995552897453308},{"category_id":8,"poly":[146.9109649658203,1100.156494140625,544.2803344726562,1100.156494140625,544.2803344726562,1184.929443359375,146.9109649658203,1184.929443359375],"score":0.9995207786560059},{"category_id":2,"poly":[148.11611938476562,99.87767791748047,318.926025390625,99.87767791748047,318.926025390625,120.70393371582031,148.11611938476562,120.70393371582031],"score":0.999351441860199},{"category_id":9,"poly":[791.7642211914062,1130.056396484375,818.6940307617188,1130.056396484375,818.6940307617188,1161.1080322265625,791.7642211914062,1161.1080322265625],"score":0.9908884763717651},{"category_id":9,"poly":[788.37060546875,1346.8450927734375,818.5010986328125,1346.8450927734375,818.5010986328125,1377.370361328125,788.37060546875,1377.370361328125],"score":0.9873985052108765},{"category_id":14,"poly":[146,1103,543,1103,543,1184,146,1184],"score":0.94,"latex":"E\\!\\left(W\\right)\\!=\\!\\frac{E\\!\\left[H^{2}\\right]}{2E\\!\\left[H\\right]}\\!=\\!\\frac{E\\!\\left[H\\right]}{2}\\!\\!\\left(1\\!+\\!\\operatorname{CV}\\!\\left(H\\right)^{2}\\right)"},{"category_id":13,"poly":[1196,354,1278,354,1278,384,1196,384],"score":0.91,"latex":"p(1-q)"},{"category_id":13,"poly":[881,415,1020,415,1020,444,881,444],"score":0.91,"latex":"(1-p)(1-q)"},{"category_id":14,"poly":[147,1333,318,1333,318,1400,147,1400],"score":0.91,"latex":"\\mathbf{CV}\\big(H\\big)\\!=\\!\\frac{\\boldsymbol{\\upsigma}_{H}}{E\\big[H\\big]}"},{"category_id":13,"poly":[1197,657,1263,657,1263,686,1197,686],"score":0.9,"latex":"(1-p)"},{"category_id":13,"poly":[213,1217,263,1217,263,1244,213,1244],"score":0.88,"latex":"E[X]"},{"category_id":13,"poly":[214,1434,245,1434,245,1459,214,1459],"score":0.87,"latex":"\\upsigma_{H}"},{"category_id":13,"poly":[324,2002,373,2002,373,2028,324,2028],"score":0.84,"latex":"30\\%"},{"category_id":13,"poly":[1209,693,1225,693,1225,717,1209,717],"score":0.83,"latex":"p"},{"category_id":13,"poly":[990,449,1007,449,1007,474,990,474],"score":0.81,"latex":"p"},{"category_id":13,"poly":[346,1277,369,1277,369,1301,346,1301],"score":0.81,"latex":"H"},{"category_id":13,"poly":[1137,661,1154,661,1154,686,1137,686],"score":0.81,"latex":"p"},{"category_id":13,"poly":[522,1432,579,1432,579,1459,522,1459],"score":0.81,"latex":"H\\left(4\\right)"},{"category_id":13,"poly":[944,540,962,540,962,565,944,565],"score":0.8,"latex":"p"},{"category_id":13,"poly":[1444,936,1461,936,1461,961,1444,961],"score":0.79,"latex":"p"},{"category_id":13,"poly":[602,1247,624,1247,624,1270,602,1270],"score":0.78,"latex":"H"},{"category_id":13,"poly":[147,1247,167,1247,167,1271,147,1271],"score":0.77,"latex":"X"},{"category_id":13,"poly":[210,1246,282,1246,282,1274,210,1274],"score":0.77,"latex":"\\operatorname{CV}(H)"},{"category_id":13,"poly":[1346,268,1361,268,1361,292,1346,292],"score":0.76,"latex":"q"},{"category_id":13,"poly":[215,957,238,957,238,981,215,981],"score":0.74,"latex":"H"},{"category_id":13,"poly":[149,956,173,956,173,981,149,981],"score":0.63,"latex":"W"},{"category_id":13,"poly":[924,841,1016,841,1016,868,924,868],"score":0.56,"latex":"8{\\cdot}00\\;\\mathrm{a.m}"},{"category_id":13,"poly":[956,871,1032,871,1032,898,956,898],"score":0.43,"latex":"20~\\mathrm{min}"},{"category_id":13,"poly":[1082,781,1112,781,1112,808,1082,808],"score":0.41,"latex":"(l)"},{"category_id":13,"poly":[697,1821,734,1821,734,1847,697,1847],"score":0.3,"latex":"^{1\\mathrm{~h~}}"}],"page_info":{"page_no":0,"height":2200,"width":1700}}]}
\ No newline at end of file
diff --git a/tests/test_tools/assets/cli_dev/cli_test_01.model.json b/tests/test_tools/assets/cli_dev/cli_test_01.model.json
new file mode 100644
index 00000000..a55f91fe
--- /dev/null
+++ b/tests/test_tools/assets/cli_dev/cli_test_01.model.json
@@ -0,0 +1,638 @@
+[
+ {
+ "layout_dets": [
+ {
+ "category_id": 1,
+ "poly": [
+ 882.4013061523438,
+ 169.93817138671875,
+ 1552.350341796875,
+ 169.93817138671875,
+ 1552.350341796875,
+ 625.8263549804688,
+ 882.4013061523438,
+ 625.8263549804688
+ ],
+ "score": 0.999992311000824
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 882.474853515625,
+ 1450.92822265625,
+ 1551.4490966796875,
+ 1450.92822265625,
+ 1551.4490966796875,
+ 1877.5712890625,
+ 882.474853515625,
+ 1877.5712890625
+ ],
+ "score": 0.9999903440475464
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 881.6513061523438,
+ 626.2058715820312,
+ 1552.1400146484375,
+ 626.2058715820312,
+ 1552.1400146484375,
+ 1450.604736328125,
+ 881.6513061523438,
+ 1450.604736328125
+ ],
+ "score": 0.9999856352806091
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 149.41075134277344,
+ 232.1595001220703,
+ 819.0465087890625,
+ 232.1595001220703,
+ 819.0465087890625,
+ 625.8865356445312,
+ 149.41075134277344,
+ 625.8865356445312
+ ],
+ "score": 0.99998539686203
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 149.3945770263672,
+ 1215.5172119140625,
+ 817.8850708007812,
+ 1215.5172119140625,
+ 817.8850708007812,
+ 1304.873291015625,
+ 149.3945770263672,
+ 1304.873291015625
+ ],
+ "score": 0.9999765157699585
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 882.6979370117188,
+ 1880.13916015625,
+ 1552.15185546875,
+ 1880.13916015625,
+ 1552.15185546875,
+ 2031.339599609375,
+ 882.6979370117188,
+ 2031.339599609375
+ ],
+ "score": 0.9999744892120361
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 148.96054077148438,
+ 743.3055419921875,
+ 818.6231689453125,
+ 743.3055419921875,
+ 818.6231689453125,
+ 1074.2369384765625,
+ 148.96054077148438,
+ 1074.2369384765625
+ ],
+ "score": 0.9999669790267944
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 148.8435516357422,
+ 1791.14306640625,
+ 818.6885375976562,
+ 1791.14306640625,
+ 818.6885375976562,
+ 2030.794189453125,
+ 148.8435516357422,
+ 2030.794189453125
+ ],
+ "score": 0.9999618530273438
+ },
+ {
+ "category_id": 0,
+ "poly": [
+ 150.7009735107422,
+ 684.0087890625,
+ 623.5106201171875,
+ 684.0087890625,
+ 623.5106201171875,
+ 717.03662109375,
+ 150.7009735107422,
+ 717.03662109375
+ ],
+ "score": 0.9999415278434753
+ },
+ {
+ "category_id": 8,
+ "poly": [
+ 146.48068237304688,
+ 1331.6737060546875,
+ 317.2640075683594,
+ 1331.6737060546875,
+ 317.2640075683594,
+ 1400.1722412109375,
+ 146.48068237304688,
+ 1400.1722412109375
+ ],
+ "score": 0.9998958110809326
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 149.42420959472656,
+ 1430.8782958984375,
+ 818.9042358398438,
+ 1430.8782958984375,
+ 818.9042358398438,
+ 1672.7386474609375,
+ 149.42420959472656,
+ 1672.7386474609375
+ ],
+ "score": 0.9998599290847778
+ },
+ {
+ "category_id": 1,
+ "poly": [
+ 149.18746948242188,
+ 172.10252380371094,
+ 818.5662231445312,
+ 172.10252380371094,
+ 818.5662231445312,
+ 230.4594268798828,
+ 149.18746948242188,
+ 230.4594268798828
+ ],
+ "score": 0.9997718334197998
+ },
+ {
+ "category_id": 0,
+ "poly": [
+ 149.0175018310547,
+ 1732.1090087890625,
+ 702.1005859375,
+ 1732.1090087890625,
+ 702.1005859375,
+ 1763.6046142578125,
+ 149.0175018310547,
+ 1763.6046142578125
+ ],
+ "score": 0.9997085928916931
+ },
+ {
+ "category_id": 2,
+ "poly": [
+ 1519.802490234375,
+ 98.59099578857422,
+ 1551.985107421875,
+ 98.59099578857422,
+ 1551.985107421875,
+ 119.48420715332031,
+ 1519.802490234375,
+ 119.48420715332031
+ ],
+ "score": 0.9995552897453308
+ },
+ {
+ "category_id": 8,
+ "poly": [
+ 146.9109649658203,
+ 1100.156494140625,
+ 544.2803344726562,
+ 1100.156494140625,
+ 544.2803344726562,
+ 1184.929443359375,
+ 146.9109649658203,
+ 1184.929443359375
+ ],
+ "score": 0.9995207786560059
+ },
+ {
+ "category_id": 2,
+ "poly": [
+ 148.11611938476562,
+ 99.87767791748047,
+ 318.926025390625,
+ 99.87767791748047,
+ 318.926025390625,
+ 120.70393371582031,
+ 148.11611938476562,
+ 120.70393371582031
+ ],
+ "score": 0.999351441860199
+ },
+ {
+ "category_id": 9,
+ "poly": [
+ 791.7642211914062,
+ 1130.056396484375,
+ 818.6940307617188,
+ 1130.056396484375,
+ 818.6940307617188,
+ 1161.1080322265625,
+ 791.7642211914062,
+ 1161.1080322265625
+ ],
+ "score": 0.9908884763717651
+ },
+ {
+ "category_id": 9,
+ "poly": [
+ 788.37060546875,
+ 1346.8450927734375,
+ 818.5010986328125,
+ 1346.8450927734375,
+ 818.5010986328125,
+ 1377.370361328125,
+ 788.37060546875,
+ 1377.370361328125
+ ],
+ "score": 0.9873985052108765
+ },
+ {
+ "category_id": 14,
+ "poly": [
+ 146,
+ 1103,
+ 543,
+ 1103,
+ 543,
+ 1184,
+ 146,
+ 1184
+ ],
+ "score": 0.94,
+ "latex": "E\\!\\left(W\\right)\\!=\\!\\frac{E\\!\\left[H^{2}\\right]}{2E\\!\\left[H\\right]}\\!=\\!\\frac{E\\!\\left[H\\right]}{2}\\!\\!\\left(1\\!+\\!\\operatorname{CV}\\!\\left(H\\right)^{2}\\right)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1196,
+ 354,
+ 1278,
+ 354,
+ 1278,
+ 384,
+ 1196,
+ 384
+ ],
+ "score": 0.91,
+ "latex": "p(1-q)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 881,
+ 415,
+ 1020,
+ 415,
+ 1020,
+ 444,
+ 881,
+ 444
+ ],
+ "score": 0.91,
+ "latex": "(1-p)(1-q)"
+ },
+ {
+ "category_id": 14,
+ "poly": [
+ 147,
+ 1333,
+ 318,
+ 1333,
+ 318,
+ 1400,
+ 147,
+ 1400
+ ],
+ "score": 0.91,
+ "latex": "\\mathbf{CV}\\big(H\\big)\\!=\\!\\frac{\\boldsymbol{\\upsigma}_{H}}{E\\big[H\\big]}"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1197,
+ 657,
+ 1263,
+ 657,
+ 1263,
+ 686,
+ 1197,
+ 686
+ ],
+ "score": 0.9,
+ "latex": "(1-p)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 213,
+ 1217,
+ 263,
+ 1217,
+ 263,
+ 1244,
+ 213,
+ 1244
+ ],
+ "score": 0.88,
+ "latex": "E[X]"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 214,
+ 1434,
+ 245,
+ 1434,
+ 245,
+ 1459,
+ 214,
+ 1459
+ ],
+ "score": 0.87,
+ "latex": "\\upsigma_{H}"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 324,
+ 2002,
+ 373,
+ 2002,
+ 373,
+ 2028,
+ 324,
+ 2028
+ ],
+ "score": 0.84,
+ "latex": "30\\%"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1209,
+ 693,
+ 1225,
+ 693,
+ 1225,
+ 717,
+ 1209,
+ 717
+ ],
+ "score": 0.83,
+ "latex": "p"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 990,
+ 449,
+ 1007,
+ 449,
+ 1007,
+ 474,
+ 990,
+ 474
+ ],
+ "score": 0.81,
+ "latex": "p"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 346,
+ 1277,
+ 369,
+ 1277,
+ 369,
+ 1301,
+ 346,
+ 1301
+ ],
+ "score": 0.81,
+ "latex": "H"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1137,
+ 661,
+ 1154,
+ 661,
+ 1154,
+ 686,
+ 1137,
+ 686
+ ],
+ "score": 0.81,
+ "latex": "p"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 522,
+ 1432,
+ 579,
+ 1432,
+ 579,
+ 1459,
+ 522,
+ 1459
+ ],
+ "score": 0.81,
+ "latex": "H\\left(4\\right)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 944,
+ 540,
+ 962,
+ 540,
+ 962,
+ 565,
+ 944,
+ 565
+ ],
+ "score": 0.8,
+ "latex": "p"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1444,
+ 936,
+ 1461,
+ 936,
+ 1461,
+ 961,
+ 1444,
+ 961
+ ],
+ "score": 0.79,
+ "latex": "p"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 602,
+ 1247,
+ 624,
+ 1247,
+ 624,
+ 1270,
+ 602,
+ 1270
+ ],
+ "score": 0.78,
+ "latex": "H"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 147,
+ 1247,
+ 167,
+ 1247,
+ 167,
+ 1271,
+ 147,
+ 1271
+ ],
+ "score": 0.77,
+ "latex": "X"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 210,
+ 1246,
+ 282,
+ 1246,
+ 282,
+ 1274,
+ 210,
+ 1274
+ ],
+ "score": 0.77,
+ "latex": "\\operatorname{CV}(H)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1346,
+ 268,
+ 1361,
+ 268,
+ 1361,
+ 292,
+ 1346,
+ 292
+ ],
+ "score": 0.76,
+ "latex": "q"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 215,
+ 957,
+ 238,
+ 957,
+ 238,
+ 981,
+ 215,
+ 981
+ ],
+ "score": 0.74,
+ "latex": "H"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 149,
+ 956,
+ 173,
+ 956,
+ 173,
+ 981,
+ 149,
+ 981
+ ],
+ "score": 0.63,
+ "latex": "W"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 924,
+ 841,
+ 1016,
+ 841,
+ 1016,
+ 868,
+ 924,
+ 868
+ ],
+ "score": 0.56,
+ "latex": "8{\\cdot}00\\;\\mathrm{a.m}"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 956,
+ 871,
+ 1032,
+ 871,
+ 1032,
+ 898,
+ 956,
+ 898
+ ],
+ "score": 0.43,
+ "latex": "20~\\mathrm{min}"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 1082,
+ 781,
+ 1112,
+ 781,
+ 1112,
+ 808,
+ 1082,
+ 808
+ ],
+ "score": 0.41,
+ "latex": "(l)"
+ },
+ {
+ "category_id": 13,
+ "poly": [
+ 697,
+ 1821,
+ 734,
+ 1821,
+ 734,
+ 1847,
+ 697,
+ 1847
+ ],
+ "score": 0.3,
+ "latex": "^{1\\mathrm{~h~}}"
+ }
+ ],
+ "page_info": {
+ "page_no": 0,
+ "height": 2200,
+ "width": 1700
+ }
+ }
+]
\ No newline at end of file
diff --git a/tests/test_tools/assets/cli_dev/cli_test_01.pdf b/tests/test_tools/assets/cli_dev/cli_test_01.pdf
new file mode 100644
index 00000000..229be9ce
Binary files /dev/null and b/tests/test_tools/assets/cli_dev/cli_test_01.pdf differ
diff --git a/tests/test_tools/assets/common/cli_test_01.pdf b/tests/test_tools/assets/common/cli_test_01.pdf
new file mode 100644
index 00000000..229be9ce
Binary files /dev/null and b/tests/test_tools/assets/common/cli_test_01.pdf differ
diff --git a/tests/test_tools/test_cli.py b/tests/test_tools/test_cli.py
new file mode 100644
index 00000000..e66b6d63
--- /dev/null
+++ b/tests/test_tools/test_cli.py
@@ -0,0 +1,125 @@
+import tempfile
+import os
+import shutil
+from click.testing import CliRunner
+
+from magic_pdf.tools.cli import cli
+
+
+def test_cli_pdf():
+ # setup
+ unitest_dir = "/tmp/magic_pdf/unittest/tools"
+ filename = "cli_test_01"
+ os.makedirs(unitest_dir, exist_ok=True)
+ temp_output_dir = tempfile.mkdtemp(dir="/tmp/magic_pdf/unittest/tools")
+
+ # run
+ runner = CliRunner()
+ result = runner.invoke(
+ cli,
+ [
+ "-p",
+ "tests/test_tools/assets/cli/pdf/cli_test_01.pdf",
+ "-o",
+ temp_output_dir,
+ ],
+ )
+
+ # check
+ assert result.exit_code == 0
+
+ base_output_dir = os.path.join(temp_output_dir, "cli_test_01/auto")
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 7000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ assert os.path.exists(os.path.join(base_output_dir, "images")) is True
+ assert os.path.isdir(os.path.join(base_output_dir, "images")) is True
+ assert os.path.exists(os.path.join(base_output_dir, "content_list.json")) is False
+
+ # teardown
+ shutil.rmtree(temp_output_dir)
+
+
+def test_cli_path():
+ # setup
+ unitest_dir = "/tmp/magic_pdf/unittest/tools"
+ os.makedirs(unitest_dir, exist_ok=True)
+ temp_output_dir = tempfile.mkdtemp(dir="/tmp/magic_pdf/unittest/tools")
+
+ # run
+ runner = CliRunner()
+ result = runner.invoke(
+ cli, ["-p", "tests/test_tools/assets/cli/path", "-o", temp_output_dir]
+ )
+
+ # check
+ assert result.exit_code == 0
+
+ filename = "cli_test_01"
+ base_output_dir = os.path.join(temp_output_dir, "cli_test_01/auto")
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 7000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ assert os.path.exists(os.path.join(base_output_dir, "images")) is True
+ assert os.path.isdir(os.path.join(base_output_dir, "images")) is True
+ assert os.path.exists(os.path.join(base_output_dir, "content_list.json")) is False
+
+ base_output_dir = os.path.join(temp_output_dir, "cli_test_02/auto")
+ filename = "cli_test_02"
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 5000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ assert os.path.exists(os.path.join(base_output_dir, "images")) is True
+ assert os.path.isdir(os.path.join(base_output_dir, "images")) is True
+ assert os.path.exists(os.path.join(base_output_dir, "content_list.json")) is False
+
+ # teardown
+ shutil.rmtree(temp_output_dir)
diff --git a/tests/test_tools/test_cli_dev.py b/tests/test_tools/test_cli_dev.py
new file mode 100644
index 00000000..56470237
--- /dev/null
+++ b/tests/test_tools/test_cli_dev.py
@@ -0,0 +1,120 @@
+import tempfile
+import os
+import shutil
+from click.testing import CliRunner
+
+from magic_pdf.tools import cli_dev
+
+
+def test_cli_pdf():
+ # setup
+ unitest_dir = "/tmp/magic_pdf/unittest/tools"
+ filename = "cli_test_01"
+ os.makedirs(unitest_dir, exist_ok=True)
+ temp_output_dir = tempfile.mkdtemp(dir="/tmp/magic_pdf/unittest/tools")
+
+ # run
+ runner = CliRunner()
+ result = runner.invoke(
+ cli_dev.cli,
+ [
+ "pdf",
+ "-p",
+ "tests/test_tools/assets/cli/pdf/cli_test_01.pdf",
+ "-j",
+ "tests/test_tools/assets/cli_dev/cli_test_01.model.json",
+ "-o",
+ temp_output_dir,
+ ],
+ )
+
+ # check
+ assert result.exit_code == 0
+
+ base_output_dir = os.path.join(temp_output_dir, "cli_test_01/auto")
+
+ r = os.stat(os.path.join(base_output_dir, "content_list.json"))
+ assert r.st_size > 5000
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 7000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ assert os.path.exists(os.path.join(base_output_dir, "images")) is True
+ assert os.path.isdir(os.path.join(base_output_dir, "images")) is True
+
+ # teardown
+ shutil.rmtree(temp_output_dir)
+
+
+def test_cli_jsonl():
+ # setup
+ unitest_dir = "/tmp/magic_pdf/unittest/tools"
+ filename = "cli_test_01"
+ os.makedirs(unitest_dir, exist_ok=True)
+ temp_output_dir = tempfile.mkdtemp(dir="/tmp/magic_pdf/unittest/tools")
+
+ def mock_read_s3_path(s3path):
+ with open(s3path, "rb") as f:
+ return f.read()
+
+ cli_dev.read_s3_path = mock_read_s3_path # mock
+
+ # run
+ runner = CliRunner()
+ result = runner.invoke(
+ cli_dev.cli,
+ [
+ "jsonl",
+ "-j",
+ "tests/test_tools/assets/cli_dev/cli_test_01.jsonl",
+ "-o",
+ temp_output_dir,
+ ],
+ )
+
+ # check
+ assert result.exit_code == 0
+
+ base_output_dir = os.path.join(temp_output_dir, "cli_test_01/auto")
+
+ r = os.stat(os.path.join(base_output_dir, "content_list.json"))
+ assert r.st_size > 5000
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 7000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ assert os.path.exists(os.path.join(base_output_dir, "images")) is True
+ assert os.path.isdir(os.path.join(base_output_dir, "images")) is True
+
+ # teardown
+ shutil.rmtree(temp_output_dir)
diff --git a/tests/test_tools/test_common.py b/tests/test_tools/test_common.py
new file mode 100644
index 00000000..0d38e1de
--- /dev/null
+++ b/tests/test_tools/test_common.py
@@ -0,0 +1,52 @@
+import tempfile
+import os
+import shutil
+
+import pytest
+
+from magic_pdf.tools.common import do_parse
+
+
+@pytest.mark.parametrize("method", ["auto", "txt", "ocr"])
+def test_common_do_parse(method):
+ # setup
+ unitest_dir = "/tmp/magic_pdf/unittest/tools"
+ filename = "fake"
+ os.makedirs(unitest_dir, exist_ok=True)
+
+ temp_output_dir = tempfile.mkdtemp(dir="/tmp/magic_pdf/unittest/tools")
+
+ # run
+ with open("tests/test_tools/assets/common/cli_test_01.pdf", "rb") as f:
+ bits = f.read()
+ do_parse(temp_output_dir, filename, bits, [], method, f_dump_content_list=True)
+
+ # check
+ base_output_dir = os.path.join(temp_output_dir, f"fake/{method}")
+
+ r = os.stat(os.path.join(base_output_dir, "content_list.json"))
+ assert r.st_size > 5000
+
+ r = os.stat(os.path.join(base_output_dir, f"{filename}.md"))
+ assert r.st_size > 7000
+
+ r = os.stat(os.path.join(base_output_dir, "middle.json"))
+ assert r.st_size > 200000
+
+ r = os.stat(os.path.join(base_output_dir, "model.json"))
+ assert r.st_size > 15000
+
+ r = os.stat(os.path.join(base_output_dir, "origin.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "layout.pdf"))
+ assert r.st_size > 500000
+
+ r = os.stat(os.path.join(base_output_dir, "spans.pdf"))
+ assert r.st_size > 500000
+
+ os.path.exists(os.path.join(base_output_dir, "images"))
+ os.path.isdir(os.path.join(base_output_dir, "images"))
+
+ # teardown
+ shutil.rmtree(temp_output_dir)