Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*.vscode
*.txt
*.__pycache__
.venv/

# compilation and distribution
__pycache__
Expand Down
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ uniparser parse report.pdf

```python
import os

parser = UniParserClient(
host="https://uniparser.dp.tech/",
api_key=os.getenv("UNIPARSER_API_KEY"),
Expand Down Expand Up @@ -152,13 +153,10 @@ import os
from uniparser_tools.api.clients import UniParserClient

# 设置 API 密钥
api_key = os.getenv('UNIPARSER_API_KEY')
api_key = os.getenv("UNIPARSER_API_KEY")

# 初始化客户端
parser = UniParserClient(
host="https://uniparser.dp.tech/",
api_key=api_key
)
parser = UniParserClient(host="https://uniparser.dp.tech/", api_key=api_key)
```

### 2. 解析 PDF 文件(科学文献推荐默认)
Expand All @@ -170,12 +168,12 @@ from uniparser_tools.common.constant import ParseMode, ParseModeTextual
result = parser.trigger_file(
pdf_path="./example.pdf",
textual=ParseModeTextual.OCRHighQuality, # high quality
equation=ParseMode.OCRHighQuality, # high quality
table=ParseMode.OCRHighQuality, # high quality
chart=ParseMode.DumpBase64, # original image base64
figure=ParseMode.DumpBase64, # original image base64
expression=ParseMode.DumpBase64, # original image base64
molecule=ParseMode.OCRFast, # fast
equation=ParseMode.OCRHighQuality, # high quality
table=ParseMode.OCRHighQuality, # high quality
chart=ParseMode.DumpBase64, # original image base64
figure=ParseMode.DumpBase64, # original image base64
expression=ParseMode.DumpBase64, # original image base64
molecule=ParseMode.OCRFast, # fast
)

if result["status"] == "success":
Expand Down Expand Up @@ -327,9 +325,9 @@ from uniparser_tools.common.constant import FormatFlag
result = parser.get_formatted(
token,
content=True,
textual=FormatFlag.Markdown, # 文本使用 Markdown
table=FormatFlag.Html, # 表格使用 HTML
equation=FormatFlag.Latex, # 公式使用 LaTeX
textual=FormatFlag.Markdown, # 文本使用 Markdown
table=FormatFlag.Html, # 表格使用 HTML
equation=FormatFlag.Latex, # 公式使用 LaTeX
)

if result["status"] == "success":
Expand Down
6 changes: 3 additions & 3 deletions skills/UniParser-Tools/references/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
```python
result = parser.trigger_file(
file_path="./document.pdf",
sync=False, # Required for async mode
callback_url="https://...", # Your callback endpoint
callback_secret="your-secret", # For signature verification
sync=False, # Required for async mode
callback_url="https://...", # Your callback endpoint
callback_secret="your-secret", # For signature verification
# ... other parse mode parameters
)
```
Expand Down
2 changes: 1 addition & 1 deletion skills/UniParser-Tools/references/data-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ for page in pages_tree:
print(f"LaTeX: {item.latex}")

# For tables, access DataFrame
if hasattr(item, 'df'):
if hasattr(item, "df"):
print(f"DataFrame:\n{item.df}")
```
1 change: 1 addition & 0 deletions skills/UniParser-Tools/references/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import hmac
import hashlib


def verify_callback(content, checksum, secret):
expected = hmac.new(secret.encode(), content.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, checksum)
Expand Down
33 changes: 15 additions & 18 deletions skills/UniParser-Tools/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,23 @@ from flask import Flask, request
app = Flask(__name__)
CALLBACK_SECRET = "your-secret-key"

@app.route('/callback', methods=['POST'])

@app.route("/callback", methods=["POST"])
def handle_callback():
data = request.json
content = data['content']
received_checksum = data['checksum']
content = data["content"]
received_checksum = data["checksum"]

# Verify signature
expected = hmac.new(
CALLBACK_SECRET.encode(),
json.dumps(content).encode(),
hashlib.sha256
).hexdigest()

expected = hmac.new(CALLBACK_SECRET.encode(), json.dumps(content).encode(), hashlib.sha256).hexdigest()

if not hmac.compare_digest(received_checksum, expected):
return {'error': 'Invalid signature'}, 401
return {"error": "Invalid signature"}, 401

# Process the result
token = data['token']
token = data["token"]
print(f"Task {token} completed!")
return {'status': 'ok'}
return {"status": "ok"}
```

## Pattern 4: Mixed Format Output
Expand All @@ -105,10 +102,10 @@ def handle_callback():
result = parser.get_formatted(
token,
content=True,
textual=FormatFlag.Markdown, # Text as Markdown
table=FormatFlag.Html, # Tables as HTML
equation=FormatFlag.Latex, # Equations as LaTeX
figure=FormatFlag.Markdown, # Figures as Markdown img
textual=FormatFlag.Markdown, # Text as Markdown
table=FormatFlag.Html, # Tables as HTML
equation=FormatFlag.Latex, # Equations as LaTeX
figure=FormatFlag.Markdown, # Figures as Markdown img
)
```

Expand Down
26 changes: 13 additions & 13 deletions skills/UniParser-Tools/references/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ clean_text = clean_scientific_text(text, strict=False)
from uniparser_tools.utils.bbox import BBox, Point

# BBox properties
bbox.area # Area
bbox.width # Width
bbox.height # Height
bbox.tl # Top-left point
bbox.br # Bottom-right point
bbox.ctr # Center point
bbox.xyxy # (x1, y1, x2, y2) tuple
bbox.xywh # (x, y, w, h) tuple
bbox.area # Area
bbox.width # Width
bbox.height # Height
bbox.tl # Top-left point
bbox.br # Bottom-right point
bbox.ctr # Center point
bbox.xyxy # (x1, y1, x2, y2) tuple
bbox.xywh # (x, y, w, h) tuple

# BBox operations
bbox.iou(other) # Intersection over Union
bbox.iof(other) # Intersection over Foreground
bbox.iou(other) # Intersection over Union
bbox.iof(other) # Intersection over Foreground
bbox.intersection(other) # Intersection box
bbox.union(other) # Union box
bbox.expand(pix, wh) # Expand by pixels
bbox.shrink(pix, wh) # Shrink by pixels
bbox.union(other) # Union box
bbox.expand(pix, wh) # Expand by pixels
bbox.shrink(pix, wh) # Shrink by pixels
```

## Text Processing
Expand Down
11 changes: 11 additions & 0 deletions uniparser_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# UniParser Agent

UniParser Agent 是基于 UniParser 的文档处理包,提供统一的命令行入口:

- `parse`:将 PDF、图片或公开 PDF URL 解析为结构化文档。
- `vqa`:从习题、试卷、题册或答案册中提取题目、答案、解析和相关图片,并生成结构化 VQA 数据。

本包包含文档解析、LLM 调用和 pdf2vqa 流程所需的公共模块。

安装方式、配置说明、命令参数、处理流程和输出格式,请参阅
[pdf2vqa 完整文档](pdf2vqa/README.md)。
3 changes: 3 additions & 0 deletions uniparser_agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""UniParser document parsing and exam VQA extraction."""

__version__ = "0.1.0"
144 changes: 144 additions & 0 deletions uniparser_agent/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from __future__ import annotations

import json
from typing import Optional

import typer

from uniparser_agent.llm import LLMConfig, resolve_llm_config
from uniparser_agent.parse.service import parse_document
from uniparser_agent.pdf2vqa.pipeline import run_vqa_pipeline


app = typer.Typer(
name="uniparser-agent",
help="UniParser document parsing and exam VQA extraction.",
no_args_is_help=True,
)


@app.command("parse")
def parse_cmd(
input_path: str = typer.Argument(..., help="Local PDF/image path or public PDF URL."),
output_dir: Optional[str] = typer.Option(None, "-o", "--output-dir", help="Output directory."),
overwrite: bool = typer.Option(False, "--overwrite", help="Replace output directory if it exists."),
json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
) -> None:
"""Parse a document with UniParser scientific-paper defaults."""
result = parse_document(input_path, output_dir=output_dir, overwrite=overwrite)
if json_output:
typer.echo(json.dumps(result, ensure_ascii=False, indent=2))
return
typer.echo(f"Token: {result.get('token', '')}")
typer.echo(f"Pages tree: {result['pages_tree_path']}")
typer.echo(f"Markdown: {result['markdown_path']}")
typer.echo(f"Output directory: {result['output_dir']}")


@app.command("vqa")
def vqa_cmd(
input_path: Optional[str] = typer.Argument(
None,
help="Local PDF/image path or public PDF URL. Omit when using --pages-tree.",
),
output_dir: Optional[str] = typer.Option(None, "-o", "--output-dir", help="VQA output directory."),
answer_pdf: Optional[str] = typer.Option(
None,
"--answer-pdf",
help="Answer booklet PDF. Merged after the question booklet (local PDFs only).",
),
pages_tree: Optional[str] = typer.Option(
None,
"--pages-tree",
help="Skip UniParser parse and use an existing pages_tree.json.",
),
overwrite: bool = typer.Option(False, "--overwrite", help="Replace output directory if it exists."),
api_key: Optional[str] = typer.Option(
None,
"--api-key",
help="LLM API key (overrides OPENAI_API_KEY).",
envvar=[],
),
base_url: Optional[str] = typer.Option(
None,
"--base-url",
help="LLM base URL (overrides OPENAI_BASE_URL).",
),
model: Optional[str] = typer.Option(
None,
"--model",
help="LLM model name (overrides OPENAI_MODEL).",
),
enable_thinking: bool = typer.Option(
False,
"--enable-thinking/--no-enable-thinking",
help="Pass chat_template_kwargs.enable_thinking for Qwen-compatible servers.",
),
json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON."),
) -> None:
"""Parse with UniParser (unless --pages-tree), then extract VQA pairs via LLM."""
if answer_pdf and pages_tree:
raise typer.BadParameter("Use either --answer-pdf or --pages-tree, not both.")
if answer_pdf and not input_path:
raise typer.BadParameter("--answer-pdf requires the question booklet as INPUT.")
if not input_path and not pages_tree:
raise typer.BadParameter("Provide INPUT (pdf/url/image) or --pages-tree.")
if input_path and pages_tree:
raise typer.BadParameter("Use either INPUT or --pages-tree, not both.")

llm_config = _build_llm_config(
api_key=api_key,
base_url=base_url,
model=model,
enable_thinking=enable_thinking,
)
result = run_vqa_pipeline(
input_path=input_path,
answer_pdf=answer_pdf,
pages_tree_path=pages_tree,
output_dir=output_dir,
overwrite=overwrite,
llm_config=llm_config,
)
if json_output:
typer.echo(json.dumps(result, ensure_ascii=False, indent=2))
return

paths = result["paths"]
if paths.get("merged_pdf"):
typer.echo(f"Merged PDF: {paths['merged_pdf']}")
typer.echo(f"Pages tree: {paths['pages_tree']}")
typer.echo(f"Content list items: {result['n_content_items']}")
typer.echo(f"VQA images: {result.get('n_vqa_images', 0)} -> {paths.get('vqa_images', '')}")
typer.echo(f"Merged VQA pairs: {result['n_merged_vqa']}")
typer.echo(f"JSONL: {paths['merged_vqa_pairs_jsonl']}")
typer.echo(f"Markdown: {paths['merged_vqa_pairs_md']}")
if paths.get("vqa_sharegpt"):
typer.echo(f"ShareGPT: {paths['vqa_sharegpt']}")
typer.echo(f"Output directory: {paths['output_dir']}")


def _build_llm_config(
*,
api_key: Optional[str],
base_url: Optional[str],
model: Optional[str],
enable_thinking: bool,
) -> LLMConfig:
try:
return resolve_llm_config(
api_key=api_key,
base_url=base_url,
model=model,
enable_thinking=enable_thinking,
)
except ValueError as exc:
raise typer.BadParameter(str(exc)) from exc


def main() -> None:
app()


if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions uniparser_agent/llm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Shared OpenAI-compatible LLM helpers for uniparser_agent."""

from uniparser_agent.llm.client import OpenAICompatLLM
from uniparser_agent.llm.config import LLMConfig, resolve_llm_config


__all__ = [
"LLMConfig",
"OpenAICompatLLM",
"resolve_llm_config",
]
Loading
Loading