From 86f65386aee443997c7345e9f35167e87fc0ece1 Mon Sep 17 00:00:00 2001
From: LittleLucifer1 <2697699085@qq.com>
Date: Sun, 1 Mar 2026 14:35:27 +0800
Subject: [PATCH 1/4] Add beamer code
---
.../p2b_pagecontent_to_beamer_agent.py | 85 +++++++
.../p2v_beamer_code_debug_agent.py | 14 +-
.../promptstemplates/prompts_repo.py | 95 +++++--
dataflow_agent/state.py | 30 +++
dataflow_agent/toolkits/p2vtool/p2v_tool.py | 78 ++++++
dataflow_agent/workflow/registry.py | 5 +-
.../workflow/wf_paper2ppt_beamer.py | 235 ++++++++++++++++++
dataflow_agent/workflow/wf_paper2video.py | 223 +----------------
fastapi_app/workflow_adapters/wa_paper2ppt.py | 2 +-
frontend-workflow/vite.config.ts | 50 ++--
10 files changed, 557 insertions(+), 260 deletions(-)
create mode 100644 dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
create mode 100644 dataflow_agent/workflow/wf_paper2ppt_beamer.py
diff --git a/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
new file mode 100644
index 00000000..9e5e12be
--- /dev/null
+++ b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
@@ -0,0 +1,85 @@
+"""
+P2bPagecontentToBeamer agent
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+从 paper2page_content 产出的 pagecontent(结构化大纲)生成 LaTeX Beamer 代码。
+输入:pagecontent (list[dict]: title, layout_description, key_points, asset_ref)
+输出:latex_code,写入 state.beamer_code_path。
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+from dataflow_agent.state import MainState
+from dataflow_agent.toolkits.tool_manager import ToolManager
+from dataflow_agent.logger import get_logger
+from dataflow_agent.agentroles.cores.base_agent import BaseAgent
+from dataflow_agent.agentroles.cores.registry import register
+from dataflow_agent.toolkits.p2vtool.p2v_tool import extract_beamer_code
+
+log = get_logger(__name__)
+
+
+# ----------------------------------------------------------------------
+# Agent Definition
+# ----------------------------------------------------------------------
+@register("p2b_pagecontent_to_beamer")
+class P2bPagecontentToBeamer(BaseAgent):
+ """从 pagecontent(结构化大纲)生成 Beamer LaTeX 代码"""
+
+ @classmethod
+ def create(cls, tool_manager: Optional[ToolManager] = None, **kwargs):
+ return cls(tool_manager=tool_manager, **kwargs)
+
+ @property
+ def role_name(self) -> str:
+ return "p2b_pagecontent_to_beamer"
+
+ @property
+ def system_prompt_template_name(self) -> str:
+ return "system_prompt_for_p2b_pagecontent_to_beamer"
+
+ @property
+ def task_prompt_template_name(self) -> str:
+ return "task_prompt_for_p2b_pagecontent_to_beamer"
+
+ def get_task_prompt_params(self, pre_tool_results: Dict[str, Any]) -> Dict[str, Any]:
+ return {
+ "pagecontent": pre_tool_results.get("pagecontent", "[]"),
+ "output_language": pre_tool_results.get("output_language", "English"),
+ "pdf_images_working_dir": pre_tool_results.get("pdf_images_working_dir", ""),
+ }
+
+ def get_default_pre_tool_results(self) -> Dict[str, Any]:
+ return {}
+
+ def update_state_result(
+ self,
+ state: MainState,
+ result: Dict[str, Any],
+ pre_tool_results: Dict[str, Any],
+ ):
+ raw = result.get("latex_code", "") if isinstance(result, dict) else ""
+ beamer_code = ""
+ if isinstance(raw, str):
+ beamer_code = extract_beamer_code(raw)
+ if not beamer_code:
+ log.error("p2b_pagecontent_to_beamer: 未得到有效 Beamer 代码")
+ super().update_state_result(state, result, pre_tool_results)
+ return
+
+ result_path = getattr(state, "result_path", "") or ""
+ if result_path:
+ base = Path(result_path).expanduser().resolve()
+ else:
+ req = getattr(state, "request", None)
+ paper_pdf_path = getattr(req, "paper_pdf_path", "") if req else ""
+ base = Path(paper_pdf_path).expanduser().resolve().parent if paper_pdf_path else Path(".").resolve()
+ auto_dir = base / "auto"
+ auto_dir.mkdir(parents=True, exist_ok=True)
+ beamer_code_path = auto_dir / "beamer_code.tex"
+ beamer_code_path.write_text(beamer_code, encoding="utf-8")
+ state.beamer_code_path = str(beamer_code_path)
+ log.info("p2b_pagecontent_to_beamer: Beamer 代码已写入 %s", beamer_code_path)
+ super().update_state_result(state, result, pre_tool_results)
diff --git a/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py b/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
index afa15a60..eec57bac 100644
--- a/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
+++ b/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
@@ -58,6 +58,15 @@ def get_default_pre_tool_results(self) -> Dict[str, Any]:
"""若调用方未显式传入,返回默认前置工具结果"""
return {}
+ async def execute_pre_tools(self, state: MainState) -> Dict[str, Any]:
+ """先执行父类前置工具;若 state 上有 pre_tool_results(workflow 内注入),则合并进结果,保证 beamer_code/code_debug_result 能进入 prompt。"""
+ results = await super().execute_pre_tools(state)
+ inject = getattr(state, "pre_tool_results", None) or {}
+ for key in ("beamer_code", "code_debug_result"):
+ if key in inject:
+ results[key] = inject[key]
+ return results
+
# ---------- 结果写回 ----------
def update_state_result(
self,
@@ -73,9 +82,12 @@ def update_state_result(
tex_path = Path(beamer_code_path)
tex_path.write_text(beamer_code, encoding='utf-8')
- # 编译最新的tex代码
+ # 编译最新的 tex 代码并写回 state,便于调用方判断是否仍存在 error/warning
from dataflow_agent.toolkits.p2vtool.p2v_tool import compile_tex
is_beamer_wrong, is_beamer_warning, code_debug_result = compile_tex(beamer_code_path)
+ state.is_beamer_wrong = is_beamer_wrong
+ state.is_beamer_warning = is_beamer_warning
+ state.code_debug_result = code_debug_result
state.ppt_path = beamer_code_path.replace(".tex", ".pdf")
log.info(f"将更新好的beamer code写回 {beamer_code_path}")
else:
diff --git a/dataflow_agent/promptstemplates/prompts_repo.py b/dataflow_agent/promptstemplates/prompts_repo.py
index b8409aab..6204ba59 100644
--- a/dataflow_agent/promptstemplates/prompts_repo.py
+++ b/dataflow_agent/promptstemplates/prompts_repo.py
@@ -1794,23 +1794,81 @@ class Paper2VideoPrompt:
## Source Content (Markdown)
{pdf_markdown}
+"""
+
+ system_prompt_for_p2b_pagecontent_to_beamer = """
+You are an expert in LaTeX Beamer. Your task is to convert **one slide's** structured outline (pagecontent) into a **single, structurally complete, compilable** Beamer LaTeX document.
+
+**Context:** You generate slide content **one page at a time**. Each output must be a **full Beamer document** that compiles on its own (with Tectonic or TeX Live). Do not output a bare frame or fragment.
+
+**Required document structure (do not omit any part):**
+1. \\documentclass{{beamer}}
+2. Preamble: \\usetheme, \\usecolortheme (or similar), and font packages (see below).
+3. \\begin{{document}}
+4. **Exactly one** \\begin{{frame}}...\\end{{frame}} containing the slide content.
+5. \\end{{document}}
+
+**CRITICAL:** Ensure every \\begin{{frame}} has a matching \\end{{frame}}, and the document ends with \\end{{document}}. Avoid the error "!File ended while scanning use of \\frame".
+
+**Font and package rules (strict):**
+- **STRICTLY FORBIDDEN:** Times New Roman, Arial, Calibri, TeX Gyre Termes, or any non-standard TeX Live font. Use \\usepackage{{lmodern}} or default LaTeX fonts only.
+- **Do NOT use** \\usepackage{{resizebox}} (invalid/grammar issues).
+- If output_language is Chinese, you **must** include in the preamble: \\usepackage{{fontspec}} and \\usepackage{{ctex}}.
+
+**Syntax rules:**
+- **Do not use & in frame titles** (causes "Misplaced alignment tab character &"). Use "and" or comma instead.
+- **Underscore in plain text:** In LaTeX, underscore `_` is reserved for math subscripts. Any `_` in normal text (e.g. function names like generate_from_input, variable names like user_inputs, system_prompt) **must** be written as \\_ (backslash-underscore). Example: `user_inputs` → `user\\_inputs`, `generate_from_input` → `generate\\_from\\_input`. Otherwise you get "Missing $ inserted" and compilation fails.
+- Use \\alert{{}} for key terms or math symbols when appropriate.
+- For literal percent sign in text use \\% (e.g. 5\\%).
+
+**Content:** Use the given title, layout_description, key_points, and asset_ref. For image paths (e.g. in asset_ref), prepend the absolute base path given by pdf_images_working_dir and use \\includegraphics[width=0.8\\textwidth]{{...}} with \\caption and \\label. For table references (e.g. Table_2) use tabular/booktabs or a placeholder.
+
+**Output:** Return only one JSON object with key "latex_code" containing the **entire** document from \\documentclass to \\end{{document}}, ready to compile.
+"""
+
+ task_prompt_for_p2b_pagecontent_to_beamer = """
+Generate **one** LaTeX Beamer slide as a **complete, compilable document**. The input is a **single slide's** pagecontent (one JSON object). Your output must be a full Beamer file: \\documentclass{{beamer}} + preamble + \\begin{{document}} + **one** \\begin{{frame}}...\\end{{frame}} + \\end{{document}}.
+
+## Output language
+{output_language}
+
+## Images base directory (absolute path prefix for \\includegraphics)
+{pdf_images_working_dir}
+
+## This slide's pagecontent (single object)
+{pagecontent}
+
+## Format requirements
+- Font: use \\usepackage{{lmodern}} or default fonts only. **Do not use** Times New Roman, TeX Gyre Termes, resizebox.
+- Chinese: if output language is Chinese, add \\usepackage{{fontspec}} and \\usepackage{{ctex}} in the preamble.
+- No **&** in frame title (use "and" or comma).
+- **Underscores in text:** Write \\_ for every underscore in normal text (e.g. user\\_inputs, generate\\_from\\_input), or you get "Missing $ inserted".
+- Literal percent: use 5\\% not 5%.
+- Every \\begin{{frame}} must have \\end{{frame}}; document must end with \\end{{document}}.
+
+## Output format
+Return a valid JSON object with a single key "latex_code".
+
+{{
+ "latex_code": "FULL_BEAMER_DOCUMENT_WITH_ONE_FRAME_HERE"
+}}
"""
system_prompt_for_p2v_beamer_code_debug = """
-You are an expert in repairing LaTeX beamer code.
+You are an expert in repairing LaTeX beamer code.
You must preserve all slide content exactly as written (including text, figures, and layout).
-Your goal is to correct LaTeX compilation errors and return clean, compilable LaTeX code.
+Your goal is to fix LaTeX compilation **errors** and **warnings** (e.g. Overfull box) and return clean, compilable LaTeX code.
Your output must:
- Be directly compilable using **tectonic** (a simplified TeX Live)
- Never include explanations, comments, or English/Chinese text outside the LaTeX code
-
"""
task_prompt_for_p2v_beamer_code_debug = """
-(Critical!) Do not modify the file path, ignore the folloing message: "warning: accessing absolute path: "
-You are given a LaTeX beamer code for the slides of a research paper and its error information.
-You should correct these errors but do not change the slide content (e.g., text, figures and layout).
+(Critical!) Do not modify the file path; ignore the following message: "warning: accessing absolute path: "
+
+You are given a LaTeX beamer code for the slides of a research paper and its compilation log (errors and/or warnings).
+Fix the reported issues but do not change the slide content (e.g., text, figures and layout).
## Content Preservation Rules (Strict)
- You MUST NOT delete, replace, or reduce the number of figures/images.
@@ -1819,23 +1877,30 @@ class Paper2VideoPrompt:
ONLY if necessary to fix compilation or layout issues.
- Keep the slide text content unchanged as much as possible.
-## Some instruction
+## Overfull box (warning)
+When the log contains **Overfull \\hbox** or **Overfull \\vbox** (content or font too large), fix by:
+- Reducing font size (e.g. \\small, \\footnotesize in the frame or for specific blocks).
+- Reducing image/figure width or scale (e.g. width=0.7\\textwidth instead of 0.9\\textwidth).
+- Do NOT remove or truncate text or figures; only resize or rescale to fit.
+
+## Other instructions
**Font Safety**: **MUST** remove or comment out any usage of the `fontspec` package if and only if it causes errors (as it depends on system fonts).
-For instance, if you encounter the error message: Package fontspec Error: The font "Latin Modern Roman" cannot be found, just remove or comment out it and use default TeX Live fonts.
+For instance, if you see: Package fontspec Error: The font "Latin Modern Roman" cannot be found, remove or comment it out and use default TeX Live fonts.
-**Image Loading Errors**:
-If the compiler reports an image loading **error**, such as: "Unable to load picture or PDF file" or "! LaTeX Error: Cannot determine size of graphic", the model **MUST** remove the entire command responsible for loading that specific graphic.
+**Image Loading Errors**:
+If the compiler reports an image loading **error** (e.g. "Unable to load picture or PDF file" or "! LaTeX Error: Cannot determine size of graphic"), **MUST** remove the entire command that loads that graphic.
-Output Format:
-- Return a JSON object with a single key "latex_code".
+## Output format
+Return a JSON object with a single key "latex_code".
{{
"latex_code": "YOUR_GENERATED_latex_beamer_code_HERE"
}}
-# Only output latex code which should be ready to compile using tectonic (simple version of TeX Live).
+Output only the JSON; the latex code must be ready to compile with tectonic.
-The LateX beamer code is:
+The LaTeX beamer code is:
{beamer_code}
-The compilation error message is:
+
+The compilation log (errors and/or warnings) is:
{code_debug_result}
"""
diff --git a/dataflow_agent/state.py b/dataflow_agent/state.py
index a45fe0ba..34269725 100644
--- a/dataflow_agent/state.py
+++ b/dataflow_agent/state.py
@@ -164,6 +164,36 @@ class Paper2VideoState(MainState):
video_path: str = ""
+# ==================== Paper2PptBeamer 相关 State 和 Request 定义 ====================
+@dataclass
+class Paper2PptBeamerRequest(MainRequest):
+ """仅用于 PDF → Beamer PPT 工作流"""
+ paper_pdf_path: str = ""
+
+
+# ==================== Paper2PptBeamer 生成 State ======================
+@dataclass
+class Paper2PptBeamerState(MainState):
+ """用于 pagecontent → Beamer PPT 工作流(接在 paper2page_content 之后)"""
+ request: Paper2PptBeamerRequest = field(default_factory=Paper2PptBeamerRequest)
+
+ # 来自上游 paper2page_content 的产出
+ pagecontent: List[Dict[str, Any]] = field(default_factory=list)
+ result_path: str = ""
+ mineru_root: str = ""
+ minueru_output: str = "" # 论文全文/摘要,供 table_extractor 等使用
+
+ beamer_code_path: str = ""
+ is_beamer_wrong: bool = False
+ is_beamer_warning: bool = False
+ code_debug_result: str = ""
+ ppt_path: str = ""
+ img_size_debug: bool = True
+
+ # 每页单独生成时的路径列表(页序)
+ per_page_beamer_paths: List[str] = field(default_factory=list)
+ per_page_pdf_paths: List[str] = field(default_factory=list)
+
# ==================== Planning Agent 相关 State ====================
@dataclass
diff --git a/dataflow_agent/toolkits/p2vtool/p2v_tool.py b/dataflow_agent/toolkits/p2vtool/p2v_tool.py
index 18544260..7b18adf9 100644
--- a/dataflow_agent/toolkits/p2vtool/p2v_tool.py
+++ b/dataflow_agent/toolkits/p2vtool/p2v_tool.py
@@ -1121,6 +1121,84 @@ def compile_tex(beamer_code_path: str):
code_debug_result = e.stderr
return is_beamer_wrong, is_beamer_warning, code_debug_result
+
+def is_overfull_warning(code_debug_result: str) -> bool:
+ """是否包含 Overfull 类 warning(内容过高/过宽),需要尝试修复。"""
+ if not code_debug_result:
+ return False
+ return "Overfull" in code_debug_result
+
+
+def is_ignorable_warning_only(code_debug_result: str) -> bool:
+ """是否仅包含可忽略的 warning(如访问绝对路径),无需修复。"""
+ if not code_debug_result:
+ return True
+ lower = code_debug_result.lower()
+ if "warning" not in lower:
+ return True
+ # 若只有 absolute path 类提示,视为可忽略
+ if "overfull" in lower:
+ return False
+ if "absolute path" in lower or "accessing absolute path" in lower:
+ return True
+ return False
+
+
+def is_table_asset(asset_ref: Any) -> bool:
+ """asset_ref 为 Table 时通常为 'Table_1' / 'Table 2' 等形式,无实际文件路径。"""
+ if not asset_ref:
+ return False
+ return str(asset_ref).strip().lower().startswith("table")
+
+
+def ensure_minueru_output(state: Any) -> None:
+ """若 state 无 minueru_output,尝试从 mineru_root 下首个 .md 读取(供 table_extractor 使用)。"""
+ if getattr(state, "minueru_output", "") and str(state.minueru_output).strip():
+ return
+ mineru_root = getattr(state, "mineru_root", "") or ""
+ if not mineru_root:
+ return
+ root = Path(mineru_root).expanduser().resolve()
+ if not root.is_dir():
+ return
+ md_files = list(root.glob("*.md"))
+ if not md_files:
+ return
+ target = md_files[0]
+ if len(md_files) > 1:
+ for f in md_files:
+ if f.stat().st_size > target.stat().st_size:
+ target = f
+ try:
+ state.minueru_output = target.read_text(encoding="utf-8")[:30000]
+ except Exception as e:
+ log.warning("从 mineru_root 读取 md 失败: %s", e)
+
+
+def merge_pdfs(pdf_paths: List[str], output_path: Union[str, Path]) -> str:
+ """将多份 PDF 按顺序合并为一份。要求 pdf_paths 中路径存在且为 PDF。"""
+ if not pdf_paths:
+ raise ValueError("merge_pdfs: pdf_paths 不能为空")
+ try:
+ import fitz # PyMuPDF
+ except ImportError:
+ raise ImportError("merge_pdfs 需要 PyMuPDF (pip install pymupdf)")
+ out = Path(output_path).expanduser().resolve()
+ out.parent.mkdir(parents=True, exist_ok=True)
+ merged = fitz.open()
+ for p in pdf_paths:
+ path = Path(p).expanduser().resolve()
+ if not path.is_file():
+ log.warning("merge_pdfs: 跳过不存在的文件 %s", path)
+ continue
+ with fitz.open(path) as src:
+ merged.insert_pdf(src)
+ merged.save(out)
+ merged.close()
+ log.info("merge_pdfs: 已合并 %s 个 PDF -> %s", len(pdf_paths), out)
+ return str(out)
+
+
def beamer_code_validator(content: str, parsed_result: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
"""检查tex是否是正确的"""
from tempfile import TemporaryDirectory
diff --git a/dataflow_agent/workflow/registry.py b/dataflow_agent/workflow/registry.py
index 501f757a..6cfa16e0 100644
--- a/dataflow_agent/workflow/registry.py
+++ b/dataflow_agent/workflow/registry.py
@@ -8,7 +8,10 @@ class RuntimeRegistry:
def register(cls, name: str, factory: Callable):
# 同一个对象重复登记 → 忽略
if name in cls._workflows:
- if cls._workflows[name] is factory:
+ if cls._workflows[name] is factory:
+ return
+ # 同一函数被导入两次(如先被包批量 import,再作为 __main__ 执行)时,忽略第二次
+ if getattr(cls._workflows[name], "__qualname__", None) == getattr(factory, "__qualname__", None):
return
raise ValueError(
f"Workflow '{name}' already registered by "
diff --git a/dataflow_agent/workflow/wf_paper2ppt_beamer.py b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
new file mode 100644
index 00000000..9addeea2
--- /dev/null
+++ b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
@@ -0,0 +1,235 @@
+"""
+paper2ppt_beamer workflow
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+pagecontent(来自 paper2page_content)→ 每页单独生成 Beamer → 每页编译成 PDF → 合并为一份 PDF → _end_。
+调用方需先跑 paper2page_content,再传入带 pagecontent / result_path / mineru_root 的 state。
+"""
+
+from __future__ import annotations
+
+import json
+import shutil
+from pathlib import Path
+
+from dataflow_agent.state import Paper2PptBeamerRequest, Paper2PptBeamerState
+from dataflow_agent.graphbuilder.graph_builder import GenericGraphBuilder
+from dataflow_agent.workflow.registry import register
+from dataflow_agent.toolkits.p2vtool.p2v_tool import (
+ compile_tex,
+ merge_pdfs,
+ is_overfull_warning,
+ is_table_asset,
+ ensure_minueru_output,
+)
+from dataflow_agent.logger import get_logger
+
+log = get_logger(__name__)
+
+
+@register("paper2ppt_beamer_pagecontent")
+def create_paper2ppt_beamer_graph() -> GenericGraphBuilder:
+ """
+ Workflow factory: dfa run --wf paper2ppt_beamer_pagecontent
+ pagecontent → 每页 pagecontent_to_beamer + compile → merge_slides → _end_
+ """
+ builder = GenericGraphBuilder(
+ state_model=Paper2PptBeamerState,
+ entry_point="_start_",
+ )
+
+ def _request_language(state: Paper2PptBeamerState) -> str:
+ req = state.request
+ if isinstance(req, dict):
+ return req.get("language", "en")
+ return getattr(req, "language", "en")
+
+ @builder.pre_tool("pagecontent", "p2b_pagecontent_to_beamer")
+ def get_pagecontent(state: Paper2PptBeamerState):
+ pc = getattr(state, "pagecontent", None)
+ return pc or []
+
+ @builder.pre_tool("output_language", "p2b_pagecontent_to_beamer")
+ def get_output_language(state: Paper2PptBeamerState):
+ language_map = {"en": "English", "zh": "Chinese"}
+ return language_map.get(_request_language(state), "English")
+
+ @builder.pre_tool("pdf_images_working_dir", "p2b_pagecontent_to_beamer")
+ def get_pdf_images_working_dir(state: Paper2PptBeamerState):
+ result_path = getattr(state, "result_path", "") or ""
+ if result_path:
+ return str(Path(result_path).expanduser().resolve() / "auto")
+ return ""
+
+ # ----------------------------------------------------------------------
+ # NODES
+ # ----------------------------------------------------------------------
+
+ async def p2b_pagecontent_to_beamer(
+ state: Paper2PptBeamerState,
+ ) -> Paper2PptBeamerState:
+ from dataflow_agent.agentroles import create_simple_agent, create_react_agent
+
+ pages = getattr(state, "pagecontent", None) or []
+ result_path = Path(getattr(state, "result_path", "") or ".").expanduser().resolve()
+ auto_dir = result_path / "auto"
+ auto_dir.mkdir(parents=True, exist_ok=True)
+
+ max_error_retries = 2
+ max_warning_fixes = 3
+
+ p2b_agent = create_simple_agent(
+ name="p2b_pagecontent_to_beamer",
+ model_name="gpt-5.2-codex-medium",
+ temperature=0.1,
+ parser_type="json",
+ )
+ debug_agent = create_simple_agent(
+ name="p2v_beamer_code_debug",
+ model_name="gpt-5.2-codex-medium",
+ temperature=0.1,
+ parser_type="json",
+ )
+
+ per_page_beamer_paths: list[str] = []
+ per_page_pdf_paths: list[str] = []
+ full_pagecontent = list(pages)
+
+ for i, one_page in enumerate(pages):
+ log.info("生成第 %s/%s 页 Beamer 并编译", i + 1, len(pages))
+ state.pagecontent = [one_page]
+
+ # ---------- Table:asset_ref 为 Table_1 等时先跑 table_extractor,再改写 asset_ref ----------
+ asset_ref = one_page.get("asset_ref") or one_page.get("asset") or ""
+ asset_ref = str(asset_ref).strip() if asset_ref else ""
+ if asset_ref and is_table_asset(asset_ref):
+ table_img_path = one_page.get("table_img_path") or one_page.get("table_png_path") or ""
+ table_img_path = str(table_img_path).strip()
+ if not table_img_path:
+ ensure_minueru_output(state)
+ state.asset_ref = asset_ref
+ state.result_path = str(result_path)
+ table_agent = create_react_agent(
+ name="table_extractor",
+ temperature=0.1,
+ max_retries=6,
+ parser_type="json",
+ )
+ state = await table_agent.execute(state=state)
+ table_img_path = str(getattr(state, "table_img_path", "") or "").strip()
+ if table_img_path and Path(table_img_path).exists():
+ table_in_auto = auto_dir / f"table_page{i}.png"
+ shutil.copy(table_img_path, table_in_auto)
+ one_page["asset_ref"] = table_in_auto.name
+ one_page["table_img_path"] = str(table_in_auto)
+ state.pagecontent = [one_page]
+ log.info("第 %s 页表格已提取并写入 %s", i + 1, table_in_auto)
+ else:
+ log.warning("第 %s 页表格提取未得到 table_img_path,Beamer 中该页表格可能缺失", i + 1)
+
+ page_tex = auto_dir / f"page_{i}.tex"
+ is_wrong = True
+ is_warning = False
+ code_debug_result = ""
+
+ # ---------- Error 重试 ----------
+ for error_attempt in range(max_error_retries):
+ state = await p2b_agent.execute(state=state)
+ if not getattr(state, "beamer_code_path", ""):
+ log.warning("第 %s 页未得到 beamer 代码,第 %s 次重试", i + 1, error_attempt + 1)
+ continue
+ shutil.copy(state.beamer_code_path, page_tex)
+ try:
+ is_wrong, is_warning, code_debug_result = compile_tex(str(page_tex))
+ except Exception as e:
+ is_wrong, is_warning, code_debug_result = True, True, str(e)
+ log.warning("第 %s 页编译异常: %s", i + 1, e)
+ if not is_wrong:
+ break
+ log.warning("第 %s 页编译 error,第 %s/%s 次重新生成", i + 1, error_attempt + 1, max_error_retries)
+
+ if is_wrong:
+ log.warning("第 %s 页经 %s 次重试仍有 error,跳过", i + 1, max_error_retries)
+ per_page_beamer_paths.append(str(page_tex))
+ continue
+
+ # ---------- Warning 修复(Overfull):通过 state.pre_tool_results 注入,供 debug agent 的 execute_pre_tools 合并进 prompt ----------
+ if is_warning and is_overfull_warning(code_debug_result):
+ for fix_attempt in range(max_warning_fixes):
+ state.beamer_code_path = str(page_tex)
+ state.is_beamer_wrong = is_wrong
+ state.is_beamer_warning = is_warning
+ state.code_debug_result = code_debug_result
+ state.pre_tool_results = {
+ "beamer_code": page_tex.read_text(encoding="utf-8"),
+ "code_debug_result": code_debug_result,
+ }
+ state = await debug_agent.execute(state)
+
+ is_wrong = getattr(state, "is_beamer_wrong", True)
+ is_warning = getattr(state, "is_beamer_warning", False)
+ code_debug_result = getattr(state, "code_debug_result", "")
+ if is_wrong:
+ break
+ if not is_warning or not is_overfull_warning(code_debug_result):
+ break
+ log.info("第 %s 页 Overfull warning,第 %s/%s 次修复", i + 1, fix_attempt + 1, max_warning_fixes)
+
+ per_page_beamer_paths.append(str(page_tex))
+ pdf_path = page_tex.with_suffix(".pdf")
+ if pdf_path.exists():
+ per_page_pdf_paths.append(str(pdf_path))
+
+ state.pagecontent = full_pagecontent
+ state.per_page_beamer_paths = per_page_beamer_paths
+ state.per_page_pdf_paths = per_page_pdf_paths
+ log.info("每页生成与编译完成: %s 个 tex, %s 个 pdf", len(per_page_beamer_paths), len(per_page_pdf_paths))
+ return state
+
+ def merge_slides_node(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
+ log.info("开始执行 merge_slides_node")
+ pdf_paths = getattr(state, "per_page_pdf_paths", None) or []
+ if not pdf_paths:
+ log.warning("无每页 PDF,无法合并")
+ return state
+ result_path = Path(getattr(state, "result_path", "") or ".").expanduser().resolve()
+ merged_path = result_path / "auto" / "merged.pdf"
+ state.ppt_path = merge_pdfs(pdf_paths, merged_path)
+ log.info("合并完成: %s", state.ppt_path)
+ return state
+
+ def _start_(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
+ return state
+
+ def _end_(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
+ return state
+
+ nodes = {
+ "_start_": _start_,
+ "p2b_pagecontent_to_beamer": p2b_pagecontent_to_beamer,
+ "merge_slides": merge_slides_node,
+ "_end_": _end_,
+ }
+
+ edges = [
+ ("_start_", "p2b_pagecontent_to_beamer"),
+ ("p2b_pagecontent_to_beamer", "merge_slides"),
+ ("merge_slides", "_end_"),
+ ]
+
+ builder.add_nodes(nodes).add_edges(edges)
+ return builder
+
+if __name__ == "__main__":
+ import asyncio
+
+ result_path = Path("outputs/default/paper2ppt/1772284521/input")
+ pagecontent = [{'title': 'DataFlow: LLM驱动的统一数据准备与工作流自动化框架', 'layout_description': '整页居中布局,仅包含标题、副标题和汇报人信息。标题大号加粗居中,副标题为论文完整英文标题置于标题下方,作者及汇报人信息放在页面下方居中,不放任何图表。', 'key_points': ['DataFlow: An LLM-Driven Framework for Unified Data Preparation and Workflow Automation in the Era of Data-Centric AI', '作者:Hao Liang 等,机构:Peking University 等', '汇报人:XXX'], 'asset_ref': None}, {'title': '研究背景与问题:LLM时代的数据准备挑战', 'layout_description': '上方简要小结背景,两栏布局:左侧为要点式文本,右侧为对比表格示意区(可用表格或示意图说明现有系统特点对比),下方一行突出本工作目标。', 'key_points': ['LLM 发展依赖大规模、高质量、语义丰富的数据准备流程,涉及合成、精炼、过滤和领域特定转换。', '当前实践以临时脚本和松散工作流为主,缺乏统一抽象、原子算子与可优化、可重现的数据流表示。', '传统大数据引擎(Spark、Dask、Hadoop)对模型闭环、GPU高效批处理和文本语义操作支持不足,工程负担巨大。', '现有数据准备系统如 NeMo Curator、Data-Juicer 主要聚焦提取与过滤,对多步生成与语义精炼的模型闭环工作流支持有限。', '研究问题:如何构建一个以 LLM 为一等公民、可编程、可复用、可扩展的统一数据准备框架?'], 'asset_ref': None}, {'title': 'DataFlow 概览:目标、定位与整体架构', 'layout_description': '上部用一两行文字概述 DataFlow 作为统一系统的定位,中间居中放系统架构示意图(核心执行引擎+管线+CLI+Agent+生态),下方采用两列要点:左列列出六大设计目标,右列说明系统范围与工作流。', 'key_points': ['系统定位:面向多领域 LLM 数据准备的统一、自动化系统,以 LLM 驱动合成与精炼为核心,覆盖文本、数学推理、代码、Text-to-SQL、Agentic RAG 和大规模知识抽取。', '设计目标:易用性(PyTorch 风格、IDE 友好)、可扩展性(模块化算子与管线)、统一范式(跨领域抽象)、性能效率(不牺牲 SOTA 表现)、智能自动化(Agent 解释自然语言意图)、开源与社区生态。', '核心组件:全局存储抽象、统一 LLM Serving、算子库、Prompt 模板、管线 Zoo,以及基于 Python 包的扩展生态 DataFlow-Ecosystem。', '用户控制层:命令行工具链(CLI)用于脚本化执行,DataFlow-Agent 将自然语言规格翻译为可执行管线并迭代调试。', '输出形态:高质量、任务对齐的数据集,可直接用于下游 LLM 训练与评测。'], 'asset_ref': 'images/ba397b4c85a1c1bd0022e9dd145db42f9ab3f956df48273d92694b3cad820a48.jpg'}, {'title': '框架设计:存储抽象、接口层次与算子生态', 'layout_description': '左右分栏布局:左侧重点用流程步骤和要点解释全局存储抽象与算子交互模式,右侧上方放算子执行模式示意图,下方用简短 bullet 解释层次化接口(Serving/Operator/Prompt/Pipeline)。', 'key_points': ['全局存储抽象:以表格化键值结构统一表示指令、回答、CoT、评分与元数据,DataFlowStorage 提供 backend 无关的 read()/write() 接口,算子只面向逻辑视图。', '算子执行模式:遵循统一的 read–transform–write 流程,可以在不修改内部逻辑的前提下重排、复用与批处理;默认实现基于 Pandas,支持 JSON/JSONL/CSV/Parquet 等格式。', '统一 LLM Serving API:generate_from_input(user_inputs, system_prompt, json_schema) 将本地引擎(vLLM、SGLang)与在线服务(ChatGPT、Gemini)统一抽象,屏蔽批处理、重试与限流细节。', '层次化接口:算子定义可复用数据变换单元,Prompt 模板声明输入渲染和输出结构约束,管线将算子按显式依赖组合成多阶段工作流,可编译验证与优化。', '算子与生态:近 200 个可复用算子,分为生成、评估、过滤、精炼四大类,搭配 90+ Prompt 模板,并通过 Python 包实现 DataFlow-Extensions,形成可插拔、社区驱动的 DataFlow-Ecosystem。'], 'asset_ref': 'images/31c09ede8e57c6b583ac2663f145fd113a811470772998506d502e3bb5ebf3ea.jpg'}, {'title': 'DataFlow-Agent 与实验结果:自动化管线构建与性能提升', 'layout_description': '上半部分两列:左列介绍 DataFlow-Agent 的角色设计与智能管线推荐,右列概括六大用例管线(文本、数学、代码、Text-to-SQL、Agentic RAG、知识抽取)。下半部分用要点强调核心实验结果与性能增益。', 'key_points': ['DataFlow-Agent 作为编排层:基于 AgentRoles 理解自然语言规格,执行算子合成、管线规划与迭代验证,可自动构造和调试新的数据准备工作流。', '智能管线推荐:面向目标任务与数据源,自动选择合适的算子组合与模板,降低工程门槛,加速原型迭代。', '六大代表性用例:文本数据准备、数学推理数据、代码处理、Text-to-SQL 数据生成、Agentic RAG 数据构造、从网页/PDF 的大规模知识抽取。', '实验结果(部分):Text-to-SQL 管线在仅使用 <0.1M 样本的情况下,相比 250 万样本 SynSQL 提升约 +3% 执行准确率;代码管线在多个基准上平均提升超过 7%。', '统一数据集效果:将文本、数学、代码数据融合为 DataFlowInstruct-10K,仅 10K 样本即可让 Qwen2-base/Qwen2.5-base 超过在 100 万 Infinity-Instruct 上训练的同规模模型,并接近对应 Instruct 模型性能。', '整体结论:DataFlow 管线在六个场景中普遍带来 1–3 分甚至更高的性能增益,验证了统一抽象与 LLM 驱动数据合成在质量与数据效率上的优势。'], 'asset_ref': 'images/80627ebb10b377adbb7f5c301c785fa17fd0ba4b8a49b0942f308faba59aa249.jpg'}, {'title': '总结与致谢', 'layout_description': '上方 concise 总结本文贡献,中间用要点强调框架价值与未来方向,下方居中放置“致谢”字样及感谢合作者和数据/代码开源社区,不放图表。', 'key_points': ['工作总结:提出 DataFlow——一个以 LLM 为中心、具备可编程算子与 PyTorch 风格管线抽象的统一数据准备框架,系统性提升了 LLM 数据构造的可复用性、可重现性与可扩展性。', '技术贡献:构建近 200 个算子与六大高性能模板管线,提供统一 LLM Serving、全局存储、层次化接口与扩展生态,并通过 DataFlow-Agent 实现自然语言到可执行管线的自动化映射。', '实证结论:在文本、数学、代码、Text-to-SQL、Agentic RAG、知识抽取等多场景中,DataFlow 生成的数据显著提升下游 LLM 性能和数据效率,部分场景超过精心人工或专用合成数据集。', '未来方向:进一步扩展多模态与多语言算子与管线,强化分布式执行与调优能力,推动 DataFlow 成为数据中心 AI 时代社区共享的统一数据准备协议。', '致谢:感谢合作者、开源社区(模型、数据、工具)及相关项目团队对本工作的支持与启发。'], 'asset_ref': None}]
+
+ graph_builder = create_paper2ppt_beamer_graph().build()
+ state = Paper2PptBeamerState(
+ request=Paper2PptBeamerRequest(language="zh"),
+ pagecontent=pagecontent,
+ result_path=str(result_path),
+ mineru_root=str(result_path),
+ )
+ state = asyncio.run(graph_builder.ainvoke(state))
diff --git a/dataflow_agent/workflow/wf_paper2video.py b/dataflow_agent/workflow/wf_paper2video.py
index a3702a5e..ff540bf9 100644
--- a/dataflow_agent/workflow/wf_paper2video.py
+++ b/dataflow_agent/workflow/wf_paper2video.py
@@ -22,9 +22,9 @@
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
from dataflow_agent.toolkits.p2vtool.p2v_tool import (
- compile_tex, beamer_code_validator, get_image_paths, parse_script_with_cursor,
+ get_image_paths, parse_script_with_cursor,
transcribe_with_whisperx, cursor_infer, get_audio_paths, get_audio_length,
- clean_text, parser_beamer_latex, resize_latex_image,
+ clean_text,
talking_gen_per_slide, render_video_with_cursor_from_json, add_subtitles,
merge_wav_files, get_mp4_duration_ffprobe,
speech_task_wrapper_with_cloud_tts,
@@ -50,75 +50,9 @@ def create_paper2video_graph() -> GenericGraphBuilder:
entry_point="_start_") # 自行修改入口
# ----------------------------------------------------------------------
- # TOOLS (pre_tool definitions)
+ # TOOLS (pre_tool definitions,仅 paper2video 相关)
# ----------------------------------------------------------------------
- @builder.pre_tool("pdf_markdown", "p2v_extract_pdf")
- def get_markdown(state: Paper2VideoState):
- import subprocess
- paper_pdf_path = Path(state.request.get("paper_pdf_path", ""))
- if not paper_pdf_path.exists():
- log.error(f"PDF 文件不存在: {paper_pdf_path}")
- return ""
- paper_pdf_dir = paper_pdf_path.with_suffix('').parent
- if not paper_pdf_path.with_suffix('').exists():
- #fixme: 这里需要修改为部署机器上的mineru
- # run_mineru_pdf_extract(str(paper_pdf_path), str(paper_pdf_dir), "modelscope")
- pass
- paper_base_path = paper_pdf_path.with_suffix('').expanduser().resolve()
- paper_output_dir = paper_base_path
- markdown_path = paper_output_dir / "auto" / f"{paper_base_path.name}.md"
- if not markdown_path.exists():
- log.error(f"Markdown 文件不存在: {str(markdown_path)}")
- return ""
- try:
- markdown_content = markdown_path.read_text(encoding='utf-8')
- return markdown_content
- except Exception as e:
- log.error(f'读取 markdown 文件内容失败:{markdown_path}. 错误:{e}')
- return ""
-
- @builder.pre_tool("pdf_images_working_dir", "p2v_extract_pdf")
- def get_images_relative_path(state: Paper2VideoState):
- paper_pdf_path = Path(state.request.get("paper_pdf_path", ""))
- if not paper_pdf_path.exists():
- log.error(f"PDF 文件不存在: {paper_pdf_path}")
- return ""
- paper_base_path = paper_pdf_path.with_suffix('').expanduser().resolve()
- paper_output_dir = paper_base_path
- images_dir = paper_output_dir/"auto"
- if not images_dir.exists():
- log.error(f"没有生成对应的图片,MinerU 识别图像失败:{images_dir}")
- return ""
- return str(images_dir)
-
- @builder.pre_tool("output_language", "p2v_extract_pdf")
- def get_language(state: Paper2VideoState):
- language_map = {
- 'en': "English",
- 'zh': "Chinese",
- }
- language = state.request.language
- return language_map.get(language, "English")
-
- @builder.pre_tool("is_beamer_wrong", "p2v_beamer_code_debug")
- def get_is_code_wrong(state: Paper2VideoState):
- return state.is_beamer_wrong
-
- @builder.pre_tool("is_beamer_warning", "p2v_beamer_code_debug")
- def get_is_code_warning(state: Paper2VideoState):
- return state.is_beamer_warning
-
- @builder.pre_tool("code_debug_result", "p2v_beamer_code_debug")
- def get_compile_result(state: Paper2VideoState):
- return state.code_debug_result
-
- @builder.pre_tool("beamer_code", "p2v_beamer_code_debug")
- def get_beamer_code(state: Paper2VideoState):
- beamer_code_path = state.beamer_code_path
- beamer_code = Path(beamer_code_path).read_text(encoding='utf-8')
- return beamer_code
-
@builder.pre_tool("video_language", "p2v_subtitle_and_cursor")
def get_video_language(state: Paper2VideoState):
language = "Chinese" if state.request.language == "zh" else "English"
@@ -147,128 +81,8 @@ def get_video_language(state: Paper2VideoState):
# ----------------------------------------------------------------------
# ==============================================================
- # NODES
+ # NODES (仅 paper2video 相关)
# ==============================================================
- async def extract_pdf_node(state: Paper2VideoState) -> Paper2VideoState:
- from dataflow_agent.agentroles import create_vlm_agent
- log.info("开始执行extract_pdf_node节点")
- agent = create_vlm_agent(
- name="p2v_extract_pdf",
- vlm_mode="understanding", # 视觉模式: 'understanding', 'generation', 'edit'
- image_detail="high", # 图像细节: 'low', 'high', 'auto'
- model_name="gpt-4o-2024-11-20", # 视觉模型
- temperature=0.1,
- max_image_size=(2048, 2048), # 最大图像尺寸
-
- # additional_params={}, # 额外VLM参数,可以存放图片用法为:"input_image": image_path
- )
-
- state = await agent.execute(state=state)
-
- # 可选:处理执行结果
- # agent_result = state.agent_results.get(agent.role_name, {})
- # log.info(f"Agent {agent.role_name} 执行结果: {agent_result}")
-
- return state
-
- def compile_beamer_node(state: Paper2VideoState) -> Paper2VideoState:
- log.info(f"开始执行compile_beamer_node")
- beamer_code_path = state.beamer_code_path
- state.is_beamer_wrong, state.is_beamer_warning, state.code_debug_result = compile_tex(beamer_code_path)
- if not state.is_beamer_warning:
- log.info(f"Beamer 代码编译成功,无需调试")
- state.ppt_path = state.beamer_code_path.replace(".tex", ".pdf")
- return state
-
- async def beamer_code_debug_node(state: Paper2VideoState) -> Paper2VideoState:
- from dataflow_agent.agentroles import create_react_agent
- log.info(f"开始执行 p2v_beamer_code_debug node节点")
- agent = create_react_agent(
- name="p2v_beamer_code_debug",
- model_name="gpt-4o-2024-11-20",
- max_retries=10,
- validators=[beamer_code_validator],
- )
- state = await agent.execute(state)
- return state
-
- async def beamer_code_upgrade_node(state: Paper2VideoState) -> Paper2VideoState:
- log.info(f"开始执行 p2v_beamer_code_debug node节点")
- from dataflow_agent.agentroles import create_vlm_agent
- from tempfile import TemporaryDirectory
- import subprocess
- from pdf2image import convert_from_path
-
- beamer_code_path = state.beamer_code_path
- old_beamer_code = Path(beamer_code_path).read_text(encoding='utf-8')
-
- head, frames_code = parser_beamer_latex(old_beamer_code)
- final_frames = []
- doc_header = ["\\documentclass{beamer}", head, "\\begin{document}"]
- doc_footer = ["\\end{document}"]
-
- for frame_code in frames_code:
- current_frame_content = ["\\begin{frame}", frame_code, "\\end{frame}"]
-
- if "includegraphics" not in frame_code:
- final_frames.extend(current_frame_content)
- continue
-
- attempt_code = current_frame_content
- img_size_debug = True
-
- while img_size_debug:
- with TemporaryDirectory() as temp_dir_name:
- temp_dir = Path(temp_dir_name)
- # 在临时目录中创建 .tex 文件
- tex_path = temp_dir / "input.tex"
-
- full_temp_tex = doc_header + attempt_code + doc_footer
- tex_path.write_text("\n".join(full_temp_tex), encoding='utf-8')
- try:
- subprocess.run(
- ["tectonic", str(tex_path)],
- check=True, capture_output=True, text=True, cwd=temp_dir
- )
-
- frame_pdf_path = tex_path.with_suffix('.pdf')
- img_path = tex_path.with_suffix('.png')
-
- if frame_pdf_path.exists():
- images = convert_from_path(str(frame_pdf_path))
- images[0].save(str(img_path))
-
- agent = create_vlm_agent(
- name="p2v_beamer_code_upgrade",
- vlm_mode="understanding",
- model_name="gpt-4o-2024-11-20",
- additional_params={"input_image": str(img_path)},
- )
-
- state = await agent.execute(state=state)
- img_size_debug = getattr(state, 'img_size_debug', False)
-
- if img_size_debug:
- log.info(f"当前图片尺寸超出了ppt一页,需要修改:{attempt_code}")
- attempt_code = resize_latex_image(attempt_code)
- else:
- final_frames.extend(attempt_code)
- else:
- log.error("PDF 未生成,跳过调试")
- final_frames.extend(attempt_code)
- break
- except Exception as e:
- log.error(f"解析单张ppt发生了错误: {e}")
- final_frames.extend(attempt_code)
- break
- full_new_code = doc_header + final_frames + doc_footer
- Path(beamer_code_path).write_text("\n".join(full_new_code), encoding='utf-8')
- compile_tex(beamer_code_path)
- state.ppt_path = str(Path(beamer_code_path).with_suffix(".pdf"))
- log.info(f"将更新好的beamer code写回 {beamer_code_path}")
-
- return state
-
async def subtitle_and_cursor(state: Paper2VideoState) -> Paper2VideoState:
'''
@@ -756,25 +570,6 @@ def merge_all(state: Paper2VideoState):
state.video_path = str(tmp_merage_3)
return state
- async def compile_beamer_condition(state: Paper2VideoState):
- # todo: 暂时先这样判断
- if state.is_beamer_warning:
- return "p2v_beamer_code_debug"
- else:
- return "_end_"
-
-
- async def pdf2ppt_node(state: Paper2VideoState) -> Paper2VideoState:
-
- log.info(f"开始执行 pdf2ppt node节点")
- from dataflow_agent.agentroles import create_simple_agent
- # agent = create_simple_agent(
- # name=""
- # )
-
-
- return state
-
def _stage_condition(state: Paper2VideoState):
if state.request.script_stage:
log.critical("进入subtitle_and_cursor stage")
@@ -797,19 +592,13 @@ def _after_speech_condition(state: Paper2VideoState):
# ==============================================================
nodes = {
"_start_": lambda state: state,
- "p2v_extract_pdf": extract_pdf_node,
- "compile_beamer": compile_beamer_node,
- "p2v_beamer_code_debug": beamer_code_debug_node,
- "p2v_beamer_code_upgrade": beamer_code_upgrade_node,
"p2v_subtitle_and_cursor": subtitle_and_cursor,
"p2v_refine_subtitle_and_cursor": refine_subtitle_and_cursor,
"p2v_generate_speech": generate_speech,
"p2v_generate_talking_video": generate_talking_video,
"p2v_generate_cursor": generate_cursor,
- "p2v_merge": merge_all,
- "pdf2ppt": pdf2ppt_node,
-
- '_end_': lambda state: state, # 终止节点
+ "p2v_merge": merge_all,
+ "_end_": lambda state: state,
}
# ------------------------------------------------------------------
diff --git a/fastapi_app/workflow_adapters/wa_paper2ppt.py b/fastapi_app/workflow_adapters/wa_paper2ppt.py
index a6a11894..b59d60fe 100644
--- a/fastapi_app/workflow_adapters/wa_paper2ppt.py
+++ b/fastapi_app/workflow_adapters/wa_paper2ppt.py
@@ -355,7 +355,7 @@ async def run_paper2ppt_full_pipeline(req: Paper2PPTRequest) -> Paper2PPTRespons
state_pc.pagecontent = pagecontent
state_pc.result_path = final_result_path
- state_pp: Paper2FigureState = await run_workflow("paper2ppt", state_pc)
+ state_pp: Paper2FigureState = await run_workflow("paper2ppt_parallel_consistent_style", state_pc)
ppt_pdf_path = getattr(state_pp, "ppt_pdf_path", "")
ppt_pptx_path = getattr(state_pp, "ppt_pptx_path", "")
diff --git a/frontend-workflow/vite.config.ts b/frontend-workflow/vite.config.ts
index 52dcfe69..92531ab6 100644
--- a/frontend-workflow/vite.config.ts
+++ b/frontend-workflow/vite.config.ts
@@ -1,42 +1,42 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
-// export default defineConfig({
-// plugins: [react()],
-// server: {
-// port: 3123,
-// open: true,
-// allowedHosts: true,
-// proxy: {
-// '/api': {
-// target: 'http://localhost:8123',
-// changeOrigin: true,
-// },
-// '/outputs': {
-// target: 'http://localhost:8123',
-// changeOrigin: true,
-// },
-// },
-// },
-// })
-
export default defineConfig({
plugins: [react()],
server: {
- port: 3111,
+ port: 3123,
open: true,
allowedHosts: true,
proxy: {
'/api': {
- // target: 'http://localhost:8000',
- target: 'http://paper2any-test-back.nas.cpolar.cn/', // FastAPI 后端地址
+ target: 'http://localhost:8123',
changeOrigin: true,
},
'/outputs': {
- // target: 'http://localhost:8000',
- target: 'http://paper2any-test-back.nas.cpolar.cn/',
+ target: 'http://localhost:8123',
changeOrigin: true,
},
},
},
-})
\ No newline at end of file
+})
+
+// export default defineConfig({
+// plugins: [react()],
+// server: {
+// port: 3111,
+// open: true,
+// allowedHosts: true,
+// proxy: {
+// '/api': {
+// // target: 'http://localhost:8000',
+// target: 'http://paper2any-test-back.nas.cpolar.cn/', // FastAPI 后端地址
+// changeOrigin: true,
+// },
+// '/outputs': {
+// // target: 'http://localhost:8000',
+// target: 'http://paper2any-test-back.nas.cpolar.cn/',
+// changeOrigin: true,
+// },
+// },
+// },
+// })
\ No newline at end of file
From a154a2c4cf12b4e9d8bf82b4538dbcc9293fa6c4 Mon Sep 17 00:00:00 2001
From: LittleLucifer1 <2697699085@qq.com>
Date: Mon, 2 Mar 2026 10:29:24 +0800
Subject: [PATCH 2/4] Upgrade the beamer code
---
.../p2b_pagecontent_to_beamer_agent.py | 31 ++++++++++++++---
.../p2v_beamer_code_debug_agent.py | 28 +++++++++++++++-
.../workflow/wf_paper2ppt_beamer.py | 33 ++++---------------
3 files changed, 60 insertions(+), 32 deletions(-)
diff --git a/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
index 9e5e12be..83328621 100644
--- a/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
+++ b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
@@ -54,16 +54,39 @@ def get_task_prompt_params(self, pre_tool_results: Dict[str, Any]) -> Dict[str,
def get_default_pre_tool_results(self) -> Dict[str, Any]:
return {}
+ def _get_beamer_code_from_result(self, result: Dict[str, Any]) -> str:
+ """从 result 中取出 Beamer 代码,兼容规范 dict 或解析失败时的 {"raw": content}。"""
+ raw = result.get("latex_code", "") if isinstance(result, dict) else ""
+ if isinstance(raw, str) and raw:
+ code = extract_beamer_code(raw)
+ if code:
+ return code
+ # 解析失败时 result 可能为 {"raw": content},尝试从原始文本提取
+ raw_content = result.get("raw", "") if isinstance(result, dict) else ""
+ if isinstance(raw_content, str) and raw_content:
+ code = extract_beamer_code(raw_content)
+ if code:
+ return code
+ try:
+ from dataflow_agent.utils import robust_parse_json
+ parsed = robust_parse_json(raw_content)
+ if isinstance(parsed, dict):
+ raw = parsed.get("latex_code", "")
+ if isinstance(raw, str) and raw:
+ code = extract_beamer_code(raw)
+ if code:
+ return code
+ except Exception:
+ pass
+ return ""
+
def update_state_result(
self,
state: MainState,
result: Dict[str, Any],
pre_tool_results: Dict[str, Any],
):
- raw = result.get("latex_code", "") if isinstance(result, dict) else ""
- beamer_code = ""
- if isinstance(raw, str):
- beamer_code = extract_beamer_code(raw)
+ beamer_code = self._get_beamer_code_from_result(result)
if not beamer_code:
log.error("p2b_pagecontent_to_beamer: 未得到有效 Beamer 代码")
super().update_state_result(state, result, pre_tool_results)
diff --git a/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py b/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
index eec57bac..7bf617d0 100644
--- a/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
+++ b/dataflow_agent/agentroles/paper2any_agents/p2v_beamer_code_debug_agent.py
@@ -18,6 +18,7 @@
from dataflow_agent.logger import get_logger
from dataflow_agent.agentroles.cores.base_agent import BaseAgent
from dataflow_agent.agentroles.cores.registry import register
+from dataflow_agent.toolkits.p2vtool.p2v_tool import extract_beamer_code
log = get_logger(__name__)
@@ -67,6 +68,31 @@ async def execute_pre_tools(self, state: MainState) -> Dict[str, Any]:
results[key] = inject[key]
return results
+ def _get_beamer_code_from_result(self, result: Dict[str, Any]) -> str:
+ """从 result 中取出 Beamer 代码,兼容规范 dict 或解析失败时的 {"raw": content}。"""
+ raw = result.get("latex_code", "") if isinstance(result, dict) else ""
+ if isinstance(raw, str) and raw:
+ code = extract_beamer_code(raw)
+ if code:
+ return code
+ raw_content = result.get("raw", "") if isinstance(result, dict) else ""
+ if isinstance(raw_content, str) and raw_content:
+ code = extract_beamer_code(raw_content)
+ if code:
+ return code
+ try:
+ from dataflow_agent.utils import robust_parse_json
+ parsed = robust_parse_json(raw_content)
+ if isinstance(parsed, dict):
+ raw = parsed.get("latex_code", "")
+ if isinstance(raw, str) and raw:
+ code = extract_beamer_code(raw)
+ if code:
+ return code
+ except Exception:
+ pass
+ return ""
+
# ---------- 结果写回 ----------
def update_state_result(
self,
@@ -75,7 +101,7 @@ def update_state_result(
pre_tool_results: Dict[str, Any],
):
"""将推理结果 {latex_code: xxxx} 写回 MainState"""
- beamer_code = result.get("latex_code", '')
+ beamer_code = self._get_beamer_code_from_result(result)
beamer_code_path = state.beamer_code_path
if beamer_code and beamer_code_path:
from pathlib import Path
diff --git a/dataflow_agent/workflow/wf_paper2ppt_beamer.py b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
index 9addeea2..983dc743 100644
--- a/dataflow_agent/workflow/wf_paper2ppt_beamer.py
+++ b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
@@ -19,7 +19,6 @@
merge_pdfs,
is_overfull_warning,
is_table_asset,
- ensure_minueru_output,
)
from dataflow_agent.logger import get_logger
@@ -67,15 +66,16 @@ def get_pdf_images_working_dir(state: Paper2PptBeamerState):
async def p2b_pagecontent_to_beamer(
state: Paper2PptBeamerState,
) -> Paper2PptBeamerState:
- from dataflow_agent.agentroles import create_simple_agent, create_react_agent
+ from dataflow_agent.agentroles import create_simple_agent
pages = getattr(state, "pagecontent", None) or []
result_path = Path(getattr(state, "result_path", "") or ".").expanduser().resolve()
auto_dir = result_path / "auto"
auto_dir.mkdir(parents=True, exist_ok=True)
+ # fixme: 这里硬编码了重试的次数,后续可能需要修改
max_error_retries = 2
- max_warning_fixes = 3
+ max_warning_fixes = 5
p2b_agent = create_simple_agent(
name="p2b_pagecontent_to_beamer",
@@ -98,33 +98,12 @@ async def p2b_pagecontent_to_beamer(
log.info("生成第 %s/%s 页 Beamer 并编译", i + 1, len(pages))
state.pagecontent = [one_page]
- # ---------- Table:asset_ref 为 Table_1 等时先跑 table_extractor,再改写 asset_ref ----------
+ # asset_ref 为 Table_1 等时直接忽略,不跑 table_extractor
asset_ref = one_page.get("asset_ref") or one_page.get("asset") or ""
asset_ref = str(asset_ref).strip() if asset_ref else ""
if asset_ref and is_table_asset(asset_ref):
- table_img_path = one_page.get("table_img_path") or one_page.get("table_png_path") or ""
- table_img_path = str(table_img_path).strip()
- if not table_img_path:
- ensure_minueru_output(state)
- state.asset_ref = asset_ref
- state.result_path = str(result_path)
- table_agent = create_react_agent(
- name="table_extractor",
- temperature=0.1,
- max_retries=6,
- parser_type="json",
- )
- state = await table_agent.execute(state=state)
- table_img_path = str(getattr(state, "table_img_path", "") or "").strip()
- if table_img_path and Path(table_img_path).exists():
- table_in_auto = auto_dir / f"table_page{i}.png"
- shutil.copy(table_img_path, table_in_auto)
- one_page["asset_ref"] = table_in_auto.name
- one_page["table_img_path"] = str(table_in_auto)
- state.pagecontent = [one_page]
- log.info("第 %s 页表格已提取并写入 %s", i + 1, table_in_auto)
- else:
- log.warning("第 %s 页表格提取未得到 table_img_path,Beamer 中该页表格可能缺失", i + 1)
+ one_page["asset_ref"] = None
+ state.pagecontent = [one_page]
page_tex = auto_dir / f"page_{i}.tex"
is_wrong = True
From 50e3c27be56e80dd13391ddd9de308498dc2b0fb Mon Sep 17 00:00:00 2001
From: LittleLucifer1 <2697699085@qq.com>
Date: Tue, 17 Mar 2026 19:15:56 +0800
Subject: [PATCH 3/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=862beamer=E7=9A=84?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../p2b_pagecontent_to_beamer_agent.py | 15 +-
.../promptstemplates/prompts_repo.py | 3 +-
.../workflow/wf_paper2ppt_beamer.py | 186 +++---
fastapi_app/routers/paper2ppt.py | 37 +-
fastapi_app/schemas.py | 8 +
fastapi_app/services/paper2ppt_service.py | 9 +-
fastapi_app/workflow_adapters/wa_paper2ppt.py | 123 +++-
frontend-workflow/src/App.tsx | 4 +-
.../src/components/AppSidebar.tsx | 37 +-
.../src/components/Paper2PptBeamerPage.tsx | 597 ++++++++++++++++++
.../src/components/paper2ppt/GenerateStep.tsx | 27 +-
.../src/components/paper2ppt/OutlineStep.tsx | 49 +-
.../src/components/paper2ppt/index.tsx | 18 +-
.../src/components/paper2ppt/types.ts | 2 +
frontend-workflow/src/locales/en/common.json | 8 +-
.../src/locales/en/paper2ppt.json | 4 +
frontend-workflow/src/locales/zh/common.json | 8 +-
.../src/locales/zh/paper2ppt.json | 4 +
18 files changed, 993 insertions(+), 146 deletions(-)
create mode 100644 frontend-workflow/src/components/Paper2PptBeamerPage.tsx
diff --git a/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
index 83328621..c4dc68d5 100644
--- a/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
+++ b/dataflow_agent/agentroles/paper2any_agents/p2b_pagecontent_to_beamer_agent.py
@@ -51,6 +51,15 @@ def get_task_prompt_params(self, pre_tool_results: Dict[str, Any]) -> Dict[str,
"pdf_images_working_dir": pre_tool_results.get("pdf_images_working_dir", ""),
}
+ async def execute_pre_tools(self, state: MainState) -> Dict[str, Any]:
+ """执行前置工具;若 state 上带有 pagecontent(并行时每页的 state),则优先使用,避免用到图节点注册时捕获的全量 pagecontent。"""
+ results = await super().execute_pre_tools(state)
+ pagecontent = getattr(state, "pagecontent", None)
+ if pagecontent is not None and isinstance(pagecontent, list) and len(pagecontent) > 0:
+ results["pagecontent"] = pagecontent
+ log.debug("使用 state.pagecontent 作为本页 pagecontent(共 %s 项)", len(pagecontent))
+ return results
+
def get_default_pre_tool_results(self) -> Dict[str, Any]:
return {}
@@ -99,9 +108,9 @@ def update_state_result(
req = getattr(state, "request", None)
paper_pdf_path = getattr(req, "paper_pdf_path", "") if req else ""
base = Path(paper_pdf_path).expanduser().resolve().parent if paper_pdf_path else Path(".").resolve()
- auto_dir = base / "auto"
- auto_dir.mkdir(parents=True, exist_ok=True)
- beamer_code_path = auto_dir / "beamer_code.tex"
+ output_dir = base / "output"
+ output_dir.mkdir(parents=True, exist_ok=True)
+ beamer_code_path = output_dir / "beamer_code.tex"
beamer_code_path.write_text(beamer_code, encoding="utf-8")
state.beamer_code_path = str(beamer_code_path)
log.info("p2b_pagecontent_to_beamer: Beamer 代码已写入 %s", beamer_code_path)
diff --git a/dataflow_agent/promptstemplates/prompts_repo.py b/dataflow_agent/promptstemplates/prompts_repo.py
index 6204ba59..e1f0391b 100644
--- a/dataflow_agent/promptstemplates/prompts_repo.py
+++ b/dataflow_agent/promptstemplates/prompts_repo.py
@@ -1803,7 +1803,7 @@ class Paper2VideoPrompt:
**Required document structure (do not omit any part):**
1. \\documentclass{{beamer}}
-2. Preamble: \\usetheme, \\usecolortheme (or similar), and font packages (see below).
+2. Preamble: **must** use \\usetheme{{Madrid}} (fixed theme). You may add \\usecolortheme and font packages (see below).
3. \\begin{{document}}
4. **Exactly one** \\begin{{frame}}...\\end{{frame}} containing the slide content.
5. \\end{{document}}
@@ -1839,6 +1839,7 @@ class Paper2VideoPrompt:
{pagecontent}
## Format requirements
+- **Theme: use \\usetheme{{Madrid}} in the preamble** (fixed; do not use other themes).
- Font: use \\usepackage{{lmodern}} or default fonts only. **Do not use** Times New Roman, TeX Gyre Termes, resizebox.
- Chinese: if output language is Chinese, add \\usepackage{{fontspec}} and \\usepackage{{ctex}} in the preamble.
- No **&** in frame title (use "and" or comma).
diff --git a/dataflow_agent/workflow/wf_paper2ppt_beamer.py b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
index 983dc743..a86d4275 100644
--- a/dataflow_agent/workflow/wf_paper2ppt_beamer.py
+++ b/dataflow_agent/workflow/wf_paper2ppt_beamer.py
@@ -7,9 +7,11 @@
from __future__ import annotations
+import asyncio
import json
import shutil
from pathlib import Path
+from dataclasses import replace
from dataflow_agent.state import Paper2PptBeamerRequest, Paper2PptBeamerState
from dataflow_agent.graphbuilder.graph_builder import GenericGraphBuilder
@@ -54,9 +56,9 @@ def get_output_language(state: Paper2PptBeamerState):
@builder.pre_tool("pdf_images_working_dir", "p2b_pagecontent_to_beamer")
def get_pdf_images_working_dir(state: Paper2PptBeamerState):
- result_path = getattr(state, "result_path", "") or ""
- if result_path:
- return str(Path(result_path).expanduser().resolve() / "auto")
+ mineru_root = getattr(state, "mineru_root", "") or ""
+ if mineru_root:
+ return str(Path(mineru_root).expanduser().resolve())
return ""
# ----------------------------------------------------------------------
@@ -70,22 +72,23 @@ async def p2b_pagecontent_to_beamer(
pages = getattr(state, "pagecontent", None) or []
result_path = Path(getattr(state, "result_path", "") or ".").expanduser().resolve()
- auto_dir = result_path / "auto"
- auto_dir.mkdir(parents=True, exist_ok=True)
+ output_dir = result_path / "output"
+ output_dir.mkdir(parents=True, exist_ok=True)
- # fixme: 这里硬编码了重试的次数,后续可能需要修改
- max_error_retries = 2
- max_warning_fixes = 5
+ # 未得到有效 Beamer 代码(如 LLM 500/usage_limit)或编译失败时重试
+ max_error_retries = 3
+ retry_delay_seconds = 3 # 无有效代码时延迟再试,缓解限流/usage_limit
+ max_warning_fixes = 2
p2b_agent = create_simple_agent(
name="p2b_pagecontent_to_beamer",
- model_name="gpt-5.2-codex-medium",
+ model_name="gpt-5-codex",
temperature=0.1,
parser_type="json",
)
debug_agent = create_simple_agent(
name="p2v_beamer_code_debug",
- model_name="gpt-5.2-codex-medium",
+ model_name="gpt-5-codex",
temperature=0.1,
parser_type="json",
)
@@ -94,69 +97,105 @@ async def p2b_pagecontent_to_beamer(
per_page_pdf_paths: list[str] = []
full_pagecontent = list(pages)
- for i, one_page in enumerate(pages):
- log.info("生成第 %s/%s 页 Beamer 并编译", i + 1, len(pages))
- state.pagecontent = [one_page]
-
- # asset_ref 为 Table_1 等时直接忽略,不跑 table_extractor
- asset_ref = one_page.get("asset_ref") or one_page.get("asset") or ""
- asset_ref = str(asset_ref).strip() if asset_ref else ""
- if asset_ref and is_table_asset(asset_ref):
- one_page["asset_ref"] = None
- state.pagecontent = [one_page]
-
- page_tex = auto_dir / f"page_{i}.tex"
- is_wrong = True
- is_warning = False
- code_debug_result = ""
-
- # ---------- Error 重试 ----------
- for error_attempt in range(max_error_retries):
- state = await p2b_agent.execute(state=state)
- if not getattr(state, "beamer_code_path", ""):
- log.warning("第 %s 页未得到 beamer 代码,第 %s 次重试", i + 1, error_attempt + 1)
- continue
- shutil.copy(state.beamer_code_path, page_tex)
- try:
- is_wrong, is_warning, code_debug_result = compile_tex(str(page_tex))
- except Exception as e:
- is_wrong, is_warning, code_debug_result = True, True, str(e)
- log.warning("第 %s 页编译异常: %s", i + 1, e)
- if not is_wrong:
- break
- log.warning("第 %s 页编译 error,第 %s/%s 次重新生成", i + 1, error_attempt + 1, max_error_retries)
-
- if is_wrong:
- log.warning("第 %s 页经 %s 次重试仍有 error,跳过", i + 1, max_error_retries)
- per_page_beamer_paths.append(str(page_tex))
- continue
-
- # ---------- Warning 修复(Overfull):通过 state.pre_tool_results 注入,供 debug agent 的 execute_pre_tools 合并进 prompt ----------
- if is_warning and is_overfull_warning(code_debug_result):
- for fix_attempt in range(max_warning_fixes):
- state.beamer_code_path = str(page_tex)
- state.is_beamer_wrong = is_wrong
- state.is_beamer_warning = is_warning
- state.code_debug_result = code_debug_result
- state.pre_tool_results = {
- "beamer_code": page_tex.read_text(encoding="utf-8"),
- "code_debug_result": code_debug_result,
- }
- state = await debug_agent.execute(state)
-
- is_wrong = getattr(state, "is_beamer_wrong", True)
- is_warning = getattr(state, "is_beamer_warning", False)
- code_debug_result = getattr(state, "code_debug_result", "")
- if is_wrong:
- break
- if not is_warning or not is_overfull_warning(code_debug_result):
+ # 并行度,避免 API 限流
+ max_concurrent_pages = 4
+ semaphore = asyncio.Semaphore(max_concurrent_pages)
+
+ async def process_one_page(
+ i: int,
+ one_page: dict,
+ ) -> tuple[int, str, str | None]:
+ """处理单页,返回 (页索引, tex 路径, pdf 路径或 None)。"""
+ async with semaphore:
+ log.info("生成第 %s/%s 页 Beamer 并编译", i + 1, len(pages))
+ one_page = dict(one_page)
+ # asset_ref 为 Table_1 等时直接忽略,不跑 table_extractor
+ asset_ref = one_page.get("asset_ref") or one_page.get("asset") or ""
+ asset_ref = str(asset_ref).strip() if asset_ref else ""
+ if asset_ref and is_table_asset(asset_ref):
+ one_page["asset_ref"] = None
+
+ page_state = replace(
+ state,
+ pagecontent=[one_page],
+ beamer_code_path="",
+ is_beamer_wrong=False,
+ is_beamer_warning=False,
+ code_debug_result="",
+ )
+ page_tex = output_dir / f"page_{i}.tex"
+ is_wrong = True
+ is_warning = False
+ code_debug_result = ""
+
+ # ---------- Error 重试(含未得到有效代码,如 LLM 500/usage_limit)----------
+ for error_attempt in range(max_error_retries):
+ page_state = await p2b_agent.execute(state=page_state)
+ if not getattr(page_state, "beamer_code_path", ""):
+ log.warning("第 %s 页未得到 beamer 代码(可能 LLM 限流/500),第 %s/%s 次重试", i + 1, error_attempt + 1, max_error_retries)
+ if error_attempt < max_error_retries - 1:
+ await asyncio.sleep(retry_delay_seconds)
+ continue
+ shutil.copy(page_state.beamer_code_path, page_tex)
+ try:
+ is_wrong, is_warning, code_debug_result = compile_tex(str(page_tex))
+ except Exception as e:
+ is_wrong, is_warning, code_debug_result = True, True, str(e)
+ log.warning("第 %s 页编译异常: %s", i + 1, e)
+ if not is_wrong:
break
- log.info("第 %s 页 Overfull warning,第 %s/%s 次修复", i + 1, fix_attempt + 1, max_warning_fixes)
-
- per_page_beamer_paths.append(str(page_tex))
- pdf_path = page_tex.with_suffix(".pdf")
- if pdf_path.exists():
- per_page_pdf_paths.append(str(pdf_path))
+ log.warning("第 %s 页编译 error,第 %s/%s 次重新生成", i + 1, error_attempt + 1, max_error_retries)
+
+ if is_wrong:
+ log.warning("第 %s 页经 %s 次重试仍有 error,跳过", i + 1, max_error_retries)
+ return (i, str(page_tex), None)
+
+ # ---------- Warning 修复(Overfull)----------
+ if is_warning and is_overfull_warning(code_debug_result):
+ for fix_attempt in range(max_warning_fixes):
+ page_state.beamer_code_path = str(page_tex)
+ page_state.is_beamer_wrong = is_wrong
+ page_state.is_beamer_warning = is_warning
+ page_state.code_debug_result = code_debug_result
+ page_state.pre_tool_results = {
+ "beamer_code": page_tex.read_text(encoding="utf-8"),
+ "code_debug_result": code_debug_result,
+ }
+ page_state = await debug_agent.execute(page_state)
+ is_wrong = getattr(page_state, "is_beamer_wrong", True)
+ is_warning = getattr(page_state, "is_beamer_warning", False)
+ code_debug_result = getattr(page_state, "code_debug_result", "")
+ if is_wrong:
+ break
+ if not is_warning or not is_overfull_warning(code_debug_result):
+ break
+ log.info("第 %s 页 Overfull warning,第 %s/%s 次修复", i + 1, fix_attempt + 1, max_warning_fixes)
+
+ pdf_path = page_tex.with_suffix(".pdf")
+ pdf_str = str(pdf_path) if pdf_path.exists() else None
+ return (i, str(page_tex), pdf_str)
+
+ # 并行处理所有页,保持页序
+ results = await asyncio.gather(
+ *[process_one_page(i, one_page) for i, one_page in enumerate(pages)],
+ return_exceptions=True,
+ )
+ # 按页索引排序,保证与原始页序一致(gather 完成顺序可能乱序)
+ tex_by_index: dict[int, str] = {}
+ pdf_by_index: dict[int, str] = {}
+ for r in results:
+ if isinstance(r, Exception):
+ log.exception("某页处理异常: %s", r)
+ continue
+ i, tex_path, pdf_path = r
+ tex_by_index[i] = tex_path
+ if pdf_path:
+ pdf_by_index[i] = pdf_path
+ for idx in range(len(pages)):
+ if idx in tex_by_index:
+ per_page_beamer_paths.append(tex_by_index[idx])
+ if idx in pdf_by_index:
+ per_page_pdf_paths.append(pdf_by_index[idx])
state.pagecontent = full_pagecontent
state.per_page_beamer_paths = per_page_beamer_paths
@@ -171,7 +210,7 @@ def merge_slides_node(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
log.warning("无每页 PDF,无法合并")
return state
result_path = Path(getattr(state, "result_path", "") or ".").expanduser().resolve()
- merged_path = result_path / "auto" / "merged.pdf"
+ merged_path = result_path / "output" / "merged.pdf"
state.ppt_path = merge_pdfs(pdf_paths, merged_path)
log.info("合并完成: %s", state.ppt_path)
return state
@@ -180,6 +219,7 @@ def _start_(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
return state
def _end_(state: Paper2PptBeamerState) -> Paper2PptBeamerState:
+ log.info(f"The ppt_path is {state.ppt_path}")
return state
nodes = {
diff --git a/fastapi_app/routers/paper2ppt.py b/fastapi_app/routers/paper2ppt.py
index fc826c4f..4a011da8 100644
--- a/fastapi_app/routers/paper2ppt.py
+++ b/fastapi_app/routers/paper2ppt.py
@@ -1,11 +1,14 @@
from __future__ import annotations
import base64
+import os
from pathlib import Path
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
+from fastapi_app.config import settings
+
from fastapi_app.schemas import (
ErrorResponse,
FullPipelineRequest,
@@ -32,8 +35,8 @@ def get_service() -> Paper2PPTService:
)
async def paper2ppt_pagecontent_json(
request: Request,
- chat_api_url: str = Form(...),
- api_key: str = Form(...),
+ chat_api_url: Optional[str] = Form(""),
+ api_key: Optional[str] = Form(""),
email: Optional[str] = Form(None),
# 输入相关:支持 text/pdf/pptx/topic
input_type: str = Form(...), # 'text' | 'pdf' | 'pptx' | 'topic'
@@ -51,15 +54,23 @@ async def paper2ppt_pagecontent_json(
pdf_as_slides: str = Form("false"),
# PPT/PDF 转图片时的渲染 DPI(None 表示默认)
render_dpi: Optional[int] = Form(None),
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码
+ ppt_mode: str = Form("image_gen"),
service: Paper2PPTService = Depends(get_service),
):
"""
只跑 paper2page_content,返回 pagecontent + result_path。
+ beamer 模式下若未传 chat_api_url/api_key,使用服务端默认(DF_API_URL/DF_API_KEY)。
"""
+ _url = (chat_api_url or "").strip()
+ _key = (api_key or "").strip()
+ if ppt_mode == "beamer" and (not _url or not _key):
+ _url = _url or os.getenv("DF_API_URL", "")
+ _key = _key or os.getenv("DF_API_KEY", "")
req = PageContentRequest(
- chat_api_url=chat_api_url,
- api_key=api_key,
+ chat_api_url=_url,
+ api_key=_key,
email=email,
input_type=input_type,
text=text,
@@ -71,6 +82,7 @@ async def paper2ppt_pagecontent_json(
use_long_paper=use_long_paper,
pdf_as_slides=pdf_as_slides,
render_dpi=render_dpi,
+ ppt_mode=ppt_mode if ppt_mode in ("image_gen", "beamer") else "image_gen",
)
data = await service.get_page_content(
@@ -90,8 +102,8 @@ async def paper2ppt_pagecontent_json(
async def paper2ppt_ppt_json(
request: Request,
img_gen_model_name: str = Form(...),
- chat_api_url: str = Form(...),
- api_key: str = Form(...),
+ chat_api_url: Optional[str] = Form(""),
+ api_key: Optional[str] = Form(""),
email: Optional[str] = Form(None),
# 控制参数
style: str = Form(""),
@@ -112,18 +124,26 @@ async def paper2ppt_ppt_json(
page_id: Optional[int] = Form(None),
# 页面编辑提示词(get_down=true 时必传)
edit_prompt: Optional[str] = Form(None),
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码(beamer 模式下无逐页编辑)
+ ppt_mode: str = Form("image_gen"),
service: Paper2PPTService = Depends(get_service),
):
"""
只跑 paper2ppt:
- get_down=false:生成模式(需要 pagecontent)
- get_down=true:编辑模式(需要 page_id(0-based) + edit_prompt,pagecontent 可选)
+ beamer 模式下若未传 chat_api_url/api_key,使用服务端默认。
"""
+ _url = (chat_api_url or "").strip()
+ _key = (api_key or "").strip()
+ if ppt_mode == "beamer" and (not _url or not _key):
+ _url = _url or settings.DEFAULT_LLM_API_URL
+ _key = _key or os.getenv("DF_API_KEY", "")
req = PPTGenerationRequest(
img_gen_model_name=img_gen_model_name,
- chat_api_url=chat_api_url,
- api_key=api_key,
+ chat_api_url=_url,
+ api_key=_key,
email=email,
style=style,
aspect_ratio=aspect_ratio,
@@ -136,6 +156,7 @@ async def paper2ppt_ppt_json(
page_id=page_id,
edit_prompt=edit_prompt,
image_resolution=image_resolution,
+ ppt_mode=ppt_mode if ppt_mode in ("image_gen", "beamer") else "image_gen",
)
data = await service.generate_ppt(
diff --git a/fastapi_app/schemas.py b/fastapi_app/schemas.py
index 1dc45121..a02ce45c 100644
--- a/fastapi_app/schemas.py
+++ b/fastapi_app/schemas.py
@@ -221,6 +221,8 @@ class PageContentRequest(BaseModel):
pdf_as_slides: str = "false"
# PPT/PDF 转图片时的渲染 DPI(None 表示使用默认值)
render_dpi: Optional[int] = None
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码
+ ppt_mode: Literal["image_gen", "beamer"] = "image_gen"
class OutlineRefineRequest(BaseModel):
@@ -311,6 +313,8 @@ class PPTGenerationRequest(BaseModel):
edit_prompt: Optional[str] = None
# 图像生成分辨率(1K/2K/4K 等)
image_resolution: Optional[str] = None
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码
+ ppt_mode: Literal["image_gen", "beamer"] = "image_gen"
class FullPipelineRequest(BaseModel):
@@ -327,6 +331,8 @@ class FullPipelineRequest(BaseModel):
style: str = ""
model: str = settings.PAPER2PPT_DEFAULT_MODEL
use_long_paper: str = "false"
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码
+ ppt_mode: Literal["image_gen", "beamer"] = "image_gen"
class Paper2PPTRequest(BaseModel):
@@ -377,6 +383,8 @@ class Paper2PPTRequest(BaseModel):
all_edited_down: bool = False
use_ai_edit: bool = False
+ # 生成方式:image_gen=图生模型,beamer=Beamer 代码
+ ppt_mode: Literal["image_gen", "beamer"] = "image_gen"
def get(self, key: str, default=None):
"""
diff --git a/fastapi_app/services/paper2ppt_service.py b/fastapi_app/services/paper2ppt_service.py
index dabd6fa7..03bba6a3 100644
--- a/fastapi_app/services/paper2ppt_service.py
+++ b/fastapi_app/services/paper2ppt_service.py
@@ -189,11 +189,13 @@ async def get_page_content(
page_count=req.page_count,
use_long_paper=use_long_paper_bool,
render_dpi=req.render_dpi,
+ ppt_mode=getattr(req, "ppt_mode", "image_gen") or "image_gen",
)
resp_model = await run_paper2page_content_wf_api(p2ppt_req, result_path=run_dir)
resp_dict = resp_model.model_dump()
+ resp_dict["ppt_mode"] = getattr(req, "ppt_mode", "image_gen") or "image_gen"
if request is not None:
resp_dict["pagecontent"] = self._convert_pagecontent_paths_to_urls(
resp_dict.get("pagecontent", []), request
@@ -289,8 +291,9 @@ async def generate_ppt(
if key in item and item[key]:
item[key] = _from_outputs_url(item[key])
- # 转换字符串布尔值
- get_down_bool = str(req.get_down).lower() in ("true", "1", "yes")
+ # 转换字符串布尔值;Beamer 模式不支持逐页编辑,强制为生成模式
+ ppt_mode = getattr(req, "ppt_mode", "image_gen") or "image_gen"
+ get_down_bool = str(req.get_down).lower() in ("true", "1", "yes") and ppt_mode != "beamer"
all_edited_down_bool = str(req.all_edited_down).lower() in ("true", "1", "yes")
# 校验编辑/生成模式
@@ -320,6 +323,7 @@ async def generate_ppt(
email=req.email or "",
all_edited_down=all_edited_down_bool,
image_resolution=req.image_resolution or "2K",
+ ppt_mode=getattr(req, "ppt_mode", "image_gen") or "image_gen",
)
resp_model = await run_paper2ppt_wf_api(
@@ -382,6 +386,7 @@ async def run_full_pipeline(
style=req.style,
email=req.email or "",
use_long_paper=req.use_long_paper,
+ ppt_mode=getattr(req, "ppt_mode", "image_gen") or "image_gen",
)
resp_model = await run_paper2ppt_full_pipeline(p2ppt_req)
diff --git a/fastapi_app/workflow_adapters/wa_paper2ppt.py b/fastapi_app/workflow_adapters/wa_paper2ppt.py
index b59d60fe..28cf3a9e 100644
--- a/fastapi_app/workflow_adapters/wa_paper2ppt.py
+++ b/fastapi_app/workflow_adapters/wa_paper2ppt.py
@@ -16,7 +16,7 @@
from typing import Any, List
from dataflow_agent.logger import get_logger
-from dataflow_agent.state import Paper2FigureState
+from dataflow_agent.state import Paper2FigureState, Paper2PptBeamerState, Paper2PptBeamerRequest
from dataflow_agent.toolkits.multimodaltool.mineru_tool import _shrink_markdown
from dataflow_agent.utils import get_project_root
from dataflow_agent.workflow import run_workflow
@@ -209,6 +209,31 @@ async def run_paper2page_content_refine_wf_api(
return Paper2PPTResponse(**resp_data)
+def _beamer_per_page_pdfs_to_ppt_pages(result_path: Path, per_page_pdf_paths: list[str]) -> None:
+ """将 Beamer 每页 PDF 转为 PNG,写入 result_path/ppt_pages/page_000.png 等,供前端展示。"""
+ if not per_page_pdf_paths:
+ return
+ try:
+ from pdf2image import convert_from_path
+ except ImportError:
+ log.warning("[wa_paper2ppt] pdf2image 未安装,跳过 Beamer 每页预览图生成")
+ return
+ ppt_pages_dir = result_path / "ppt_pages"
+ ppt_pages_dir.mkdir(parents=True, exist_ok=True)
+ for i, pdf_path in enumerate(per_page_pdf_paths):
+ p = Path(pdf_path)
+ if not p.exists():
+ continue
+ try:
+ images = convert_from_path(str(p), first_page=1, last_page=1, dpi=150)
+ if images:
+ out_name = f"page_{i:03d}.png"
+ images[0].save(ppt_pages_dir / out_name, "PNG")
+ log.info("[wa_paper2ppt] Beamer 页 %s -> %s", i, out_name)
+ except Exception as e:
+ log.warning("[wa_paper2ppt] Beamer 页 %s 转 PNG 失败: %s", i, e)
+
+
async def run_paper2ppt_wf_api(
req: Paper2PPTRequest,
pagecontent: list[dict] | None = None,
@@ -287,27 +312,72 @@ async def run_paper2ppt_wf_api(
log.info(
f"[paper2ppt_wf_api] start, result_path={getattr(state, 'result_path', None)}, "
- f"pagecontent_len={len(getattr(state, 'pagecontent', []) or [])}"
+ f"pagecontent_len={len(getattr(state, 'pagecontent', []) or [])}, ppt_mode={getattr(req, 'ppt_mode', 'image_gen')}"
)
- # final_state: Paper2FigureState = await run_workflow("paper2ppt_parallel", state)
+ ppt_mode = getattr(req, "ppt_mode", "image_gen") or "image_gen"
+
+ if ppt_mode == "beamer":
+ # Beamer 路径:pagecontent → paper2ppt_beamer_pagecontent → merged PDF,并生成每页 PNG 供前端展示
+ if not base_dir:
+ raise ValueError("result_path is required for beamer mode")
+ pc = getattr(state, "pagecontent", []) or []
+ if not pc:
+ return Paper2PPTResponse(success=False, ppt_pdf_path="", ppt_pptx_path="", pagecontent=[], result_path=str(base_dir))
+ lang = getattr(req, "language", "en") or "en"
+ # 将 API 配置从请求传入,否则 agent 会用默认/空 key 导致 401
+ api_url = getattr(req, "chat_api_url", "") or ""
+ api_key = getattr(req, "api_key", "") or getattr(req, "chat_api_key", "") or ""
+ model = getattr(req, "model", "") or "gpt-4o"
+ beamer_req = Paper2PptBeamerRequest(
+ language=lang,
+ chat_api_url=api_url,
+ api_key=api_key,
+ chat_api_key=api_key,
+ model=model,
+ )
+ state_beamer = Paper2PptBeamerState(
+ request=beamer_req,
+ pagecontent=pc,
+ result_path=str(base_dir),
+ mineru_root=str(base_dir / "input" / "auto"),
+ minueru_output=getattr(state, "mineru_output", "") or "",
+ )
+ final_beamer = await run_workflow("paper2ppt_beamer_pagecontent", state_beamer)
+ if isinstance(final_beamer, dict):
+ ppt_pdf_path = final_beamer.get("ppt_path") or ""
+ per_page_pdf_paths = final_beamer.get("per_page_pdf_paths") or []
+ pagecontent = final_beamer.get("pagecontent") or []
+ else:
+ ppt_pdf_path = getattr(final_beamer, "ppt_path", "") or ""
+ per_page_pdf_paths = getattr(final_beamer, "per_page_pdf_paths", []) or []
+ pagecontent = getattr(final_beamer, "pagecontent", []) or []
+ _beamer_per_page_pdfs_to_ppt_pages(base_dir, per_page_pdf_paths)
+ resp_data = {
+ "success": True,
+ "ppt_pdf_path": str(ppt_pdf_path) if ppt_pdf_path else "",
+ "ppt_pptx_path": "",
+ "pagecontent": pagecontent,
+ "result_path": str(base_dir),
+ }
+ return Paper2PPTResponse(**resp_data)
+
+ # 图生模型路径
log.critical(f'[wa_paper2ppt] req.ref_img 路径 {req.ref_img}')
final_state: Paper2FigureState = await run_workflow("paper2ppt_parallel_consistent_style", state)
- # 提取关键输出
ppt_pdf_path = getattr(final_state, "ppt_pdf_path", "")
ppt_pptx_path = getattr(final_state, "ppt_pptx_path", "")
final_pagecontent = getattr(final_state, "pagecontent", []) or []
final_result_path = getattr(final_state, "result_path", result_path or "")
- resp_data: dict[str, Any] = {
+ resp_data = {
"success": True,
"ppt_pdf_path": str(ppt_pdf_path) if ppt_pdf_path else "",
"ppt_pptx_path": str(ppt_pptx_path) if ppt_pptx_path else "",
"pagecontent": final_pagecontent,
"result_path": final_result_path,
}
-
return Paper2PPTResponse(**resp_data)
@@ -347,26 +417,57 @@ async def run_paper2ppt_full_pipeline(req: Paper2PPTRequest) -> Paper2PPTRespons
final_result_path = getattr(state_pc, "result_path", str(result_root))
# ---------- 第二步:paper2ppt ----------
- # 复用 state_pc 继续执行 paper2ppt,避免丢失中间状态
+ ppt_mode = getattr(req, "ppt_mode", "image_gen") or "image_gen"
log.info(
- f"[paper2ppt_full_pipeline] step2 paper2ppt, "
+ f"[paper2ppt_full_pipeline] step2 paper2ppt, ppt_mode={ppt_mode}, "
f"result_path={final_result_path}, pagecontent_len={len(pagecontent)}"
)
+
+ if ppt_mode == "beamer":
+ result_root_path = Path(final_result_path)
+ api_url = getattr(req, "chat_api_url", "") or ""
+ api_key = getattr(req, "api_key", "") or getattr(req, "chat_api_key", "") or ""
+ model = getattr(req, "model", "") or "gpt-4o"
+ beamer_req = Paper2PptBeamerRequest(
+ language=getattr(req, "language", "en") or "en",
+ chat_api_url=api_url,
+ api_key=api_key,
+ chat_api_key=api_key,
+ model=model,
+ )
+ state_beamer = Paper2PptBeamerState(
+ request=beamer_req,
+ pagecontent=pagecontent,
+ result_path=final_result_path,
+ mineru_root=str(result_root_path / "input" / "auto"),
+ minueru_output=getattr(state_pc, "minueru_output", "") or "",
+ )
+ final_beamer = await run_workflow("paper2ppt_beamer_pagecontent", state_beamer)
+ ppt_pdf_path = getattr(final_beamer, "ppt_path", "") or ""
+ per_page_pdf_paths = getattr(final_beamer, "per_page_pdf_paths", []) or []
+ _beamer_per_page_pdfs_to_ppt_pages(result_root_path, per_page_pdf_paths)
+ resp_data = {
+ "success": True,
+ "ppt_pdf_path": str(ppt_pdf_path) if ppt_pdf_path else "",
+ "ppt_pptx_path": "",
+ "pagecontent": getattr(final_beamer, "pagecontent", []) or pagecontent,
+ "result_path": final_result_path,
+ }
+ return Paper2PPTResponse(**resp_data)
+
state_pc.pagecontent = pagecontent
state_pc.result_path = final_result_path
-
state_pp: Paper2FigureState = await run_workflow("paper2ppt_parallel_consistent_style", state_pc)
ppt_pdf_path = getattr(state_pp, "ppt_pdf_path", "")
ppt_pptx_path = getattr(state_pp, "ppt_pptx_path", "")
final_pagecontent = getattr(state_pp, "pagecontent", []) or []
- resp_data: dict[str, Any] = {
+ resp_data = {
"success": True,
"ppt_pdf_path": str(ppt_pdf_path) if ppt_pdf_path else "",
"ppt_pptx_path": str(ppt_pptx_path) if ppt_pptx_path else "",
"pagecontent": final_pagecontent,
"result_path": final_result_path,
}
-
return Paper2PPTResponse(**resp_data)
diff --git a/frontend-workflow/src/App.tsx b/frontend-workflow/src/App.tsx
index 624f865e..0c923a5b 100644
--- a/frontend-workflow/src/App.tsx
+++ b/frontend-workflow/src/App.tsx
@@ -3,6 +3,7 @@ import ParticleBackground from './components/ParticleBackground';
import Paper2GraphTechExpPage from './components/Paper2GraphTechExpPage';
import Paper2GraphDrawioPage from './components/Paper2GraphDrawioPage';
import Paper2PptPage from './components/Paper2PptPage';
+import Paper2PptBeamerPage from './components/Paper2PptBeamerPage';
import Pdf2PptPage from './components/Pdf2PptPage';
import Image2PptPage from './components/Image2PptPage';
import Image2DrawioPage from './components/Image2DrawioPage';
@@ -23,7 +24,7 @@ import { AppSidebar } from './components/AppSidebar';
function App() {
const { t } = useTranslation('common');
- const [activePage, setActivePage] = useState<'paper2figure-tech-exp' | 'paper2figure-model-drawio' | 'paper2drawio-ai' | 'paper2ppt' | 'paper2video' | 'pdf2ppt' | 'image2ppt' | 'image2drawio' | 'ppt2polish' | 'knowledge' | 'files' | 'paper2drawio' | 'paper2rebuttal'>('paper2figure-tech-exp');
+ const [activePage, setActivePage] = useState<'paper2figure-tech-exp' | 'paper2figure-model-drawio' | 'paper2drawio-ai' | 'paper2ppt' | 'paper2ppt_beamer' | 'paper2video' | 'pdf2ppt' | 'image2ppt' | 'image2drawio' | 'ppt2polish' | 'knowledge' | 'files' | 'paper2drawio' | 'paper2rebuttal'>('paper2figure-tech-exp');
const [showFilesModal, setShowFilesModal] = useState(false);
const [showAccountModal, setShowAccountModal] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -84,6 +85,7 @@ function App() {
{activePage === 'paper2figure-model-drawio' &&
Beamer PDF 已生成,可下载。
- {downloadUrl && ( -