Skill: ljk00000/ynu-papergraphgeneration-openclaw (version 1.0.0)

--- name: paper-diagram description: > 多模态论文可视化引擎 — 从 PDF 或纯文本论文自动生成学术插图。 支持：全篇扫描识别可图化内容、双编码器架构图/算法流程图/动机图生成、 自校核机制、LaTeX/Word 图注输出、Matplotlib 结果图精确绘图。 也包含独立的 PDF → Text 提取工具。 homepage: https://github.com/yourrepo/paper-diagram-skill metadata: { "openclaw": { "emoji": "📊", "requires": { "bins": ["python"], "env": [ "BANANA2_API_URL", "BANANA2_API_KEY" ] }, "primaryEnv": "BANANA2_API_KEY", "envFallbacks": { "BANANA2_API_KEY": ["ACEDATA_API_KEY", "PAPER_DIAGRAM_API_KEY"], "BANANA2_API_URL": ["PAPER_DIAGRAM_API_URL"] } } } --- # paper-diagram-skill 从论文（PDF 或纯文本）自动生成学术插图的完整工具链。 ## 工具概览 | 工具 | 功能 | 调用方式 | |------|------|----------| | **draw.py** | 论文可视化主流程 | `python draw.py [args]` | | **pdf_to_text.py** | PDF → 纯文本提取 | `python pdf_to_text.py [args]` | --- ## 工具 1：PDF → Text 提取 将 PDF 文件转换为纯文本，供 draw.py 使用。 ```bash # 基本用法 python {baseDir}/scripts/pdf_to_text.py "/path/to/paper.pdf" # 限制页数（取前 20 页） python {baseDir}/scripts/pdf_to_text.py "/path/to/paper.pdf" --max-pages 20 # 输出到文件 python {baseDir}/scripts/pdf_to_text.py "/path/to/paper.pdf" -o extracted.txt ``` **支持的后端（自动选择）：** pdfminer.six → PyMuPDF → pdfplumber → PyPDF2 → pypdf > 注意：推荐先安装 `pip install pdfminer.six`，提取质量最高。 --- ## 工具 2：论文图生成 ### 快速开始 ```bash # 方式 A：从 PDF 直接生成（自动提取 + 画图） python {baseDir}/scripts/draw.py \ --pdf-path "/path/to/paper.pdf" \ --user-requirement "Transformer 架构图" \ --api-url "https://api.example.com/generate" \ --api-key "sk-your-key" # 方式 B：扫描模式（自动识别所有可图化内容） python {baseDir}/scripts/draw.py \ --pdf-path "/path/to/paper.pdf" \ --mode scan # 方式 C：批量生成（选定的图） python {baseDir}/scripts/draw.py \ --pdf-path "/path/to/paper.pdf" \ --mode scan \ --scan-results "<base64-encoded-results>" \ --selected-ids "1,3,5" \ --api-url "..." --api-key "..." # 方式 D：直接传入论文文本 python {baseDir}/scripts/draw.py \ --paper-content "论文纯文本内容..." \ --user-requirement "画一个算法流程图" \ --style cvpr ``` ### 核心参数 | 参数 | 说明 | 默认值 | |------|------|--------| | `--pdf-path` | PDF 文件路径（会自动提取文本） | — | | `--paper-content` | 直接传入论文纯文本 | — | | `--user-requirement` | 用户对图片的描述 | — | | `--mode` | `single`(单图) 或 `scan`(全篇扫描) | `single` | | `--api-url` | 生图 API 地址 | 环境变量 | | `--api-key` | 生图 API 密钥 | 环境变量 | | `--style` | 学术风格: `cvpr` `neurips` `icml` `nature` | `cvpr` | | `--skip-check` | 跳过自校核 | False | | `--section-ref` | 对应论文章节（如 `Section 3.1`） | — | ### 环境变量 生图 API 凭证按以下优先级自动解析，**任意一个有值即可**： | 优先级 | API Key 变量 | API URL 变量 | |--------|-------------|-------------| | 1 (推荐) | `BANANA2_API_KEY` | `BANANA2_API_URL` | | 2 | `ACEDATA_API_KEY` | `PAPER_DIAGRAM_API_URL` | | 3 | `PAPER_DIAGRAM_API_KEY` | — | > 未设置 URL 时默认 `https://api.acedata.cloud/nano-banana/images` 推荐在 `~/.openclaw/openclaw.json` 中配置： ```json { "skills": { "paper-diagram": { "env": { "BANANA2_API_URL": "https://api.acedata.cloud/nano-banana/images", "BANANA2_API_KEY": "your-api-key-here", "PAPER_DIAGRAM_LLM_URL": "https://your-llm-api/v1/chat/completions", "PAPER_DIAGRAM_LLM_KEY": "your-llm-key-here", "PAPER_DIAGRAM_LLM_MODEL": "gpt-4" } } } } ``` ### 生成的图类型 | 类型 | 说明 | 生成方式 | |------|------|----------| | `teaser` | 动机/概念对比图 | Mermaid → AI 生图 | | `architecture` | 系统架构图 | Mermaid → AI 生图 | | `flowchart` | 算法流程图 | Mermaid → AI 生图 | | `environment` | 实验环境图 | Mermaid → AI 生图 | | `results` | 结果对比图 | Matplotlib 代码绘图（精确数据）| ### 学术风格预设 | 预设 | 场景 | 主色调 | |------|------|--------| | `cvpr` | CV/CG 会议 | 蓝色系 | | `neurips` | ML/NLP 会议 | 紫橙系 | | `icml` | ML 会议 | 蓝红绿系 | | `nature` | Science/Nature 期刊 | 柔蓝柔绿 | ### 输出 - **图片文件**：保存到 `outputs/` 目录（或设置的输出目录） - **LaTeX 图注**：可直接复制到论文 - **Word 图注**：纯文本格式 - **Mermaid 拓扑**：可导出用于其他绘图工具 ### 自校核机制 生成图前会自动对比拓扑描述与原文： - ✅ 一致 → 直接生成 - ⚠️ 小问题 → 询问用户（接受 / 重新生成 / 手动修改） - ❌ 严重错误 → 建议重新生成 --- ## 推荐工作流 ``` 论文 PDF │ ▼ pdf_to_text.py ──→ 纯文本 │ │ ▼ ▼ draw.py (scan) draw.py (single) │ │ ▼ ▼ 扫描报告 指定描述 → 生成单张图 │ ▼ 用户选择 (e.g. 1,3,5) │ ▼ draw.py (batch) ──→ 多张图 + LaTeX 图注 ``` ## 安装依赖 ```bash pip install pdfminer.six requests ``` 可选（提升 PDF 提取质量）： ```bash pip install pymupdf pdfplumber pypdf ```

path: readme.md
content: # 论文架构图生成器 (Paper Diagram Generator)

## 📌 简介
本技能专为科研人员设计。它通过 OpenClaw 解析学术论文的核心框架或摘要文本，自动提取逻辑结构与视觉实体，并将其转化为高质量的绘图提示词，最终调用图像大模型（如 Banana Pro / Zimage）生成符合顶会审美（CVPR/NeurIPS）的架构示意图。

## ⚙️ 工作流
1. **输入层**：接收用户自定义要求和论文文本。
2. **中控层**：OpenClaw 总结提取结构化逻辑，生成纯英文 Prompt。
3. **执行层**：调用文生图 API 渲染图片并返回。

## 🚀 使用方法
在 OpenClaw 中触发该技能，提供论文的摘要或核心 Methodology 段落，并指定想要的图片风格（如 Flat Design）。

path: scripts/chart_generator.py
content: """
chart_generator.py - 将结构化实验数据转化为 Matplotlib 绘图代码
这是与生图模型完全不同的路径：结果图不用生图，用代码画
"""


def generate_bar_chart_code(extraction_result: str) -> str:
    """生成柱状对比图 Python 代码"""
    prompt = f"""你是一个数据可视化工程师。根据以下实验数据，生成一段完整的、可直接运行的 Python + Matplotlib 代码来绘制柱状对比图。

## 严格规则
1. **输出必须是完整可运行的 Python 代码**，包含所有 import
2. **使用 matplotlib**，不要用 seaborn 或其他库（保持兼容性）
3. **颜色使用学术配色**：蓝色系（#2171B5, #4292C6, #6BAED6）为主本文方法，其他颜色区分基线
4. **Y 轴标签使用英文**，刻度清晰
5. **每个柱子顶部显示数值**（ annotate）
6. **添加图例 (legend)** 和网格线 (grid)
7. **图表标题和轴标签全部使用英文**
8. **保存到文件**：output_bar.png（300 DPI，高分辨率）
9. **绝对不能有任何中文字符**

## 代码模板参考
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

methods = ['Method A', 'Method B', 'Ours']
scores = [72.3, 75.8, 81.2]
colors = ['#CCCCCC', '#AAAAAA', '#2171B5']

fig, ax = plt.subplots(figsize=(6, 4), dpi=300)
bars = ax.bar(methods, scores, color=colors, width=0.5, edgecolor='black', linewidth=0.8)
for bar, score in zip(bars, scores):
    ax.annotate(f'{{score:.1f}}', xy=(bar.get_x() + bar.get_width()/2, bar.get_height()),
                xytext=(0, 3), textcoords='offset points', ha='center', fontsize=10)
ax.set_ylabel('Accuracy (%)', fontsize=12)
ax.set_title('Performance Comparison on Dataset X', fontsize=13)
ax.set_ylim(0, 100)
ax.grid(axis='y', linestyle='--', alpha=0.7)
ax.legend(['Method A', 'Method B', 'Ours'], loc='lower right')
plt.tight_layout()
plt.savefig('output_bar.png', dpi=300, bbox_inches='tight')
plt.close()
print('[DONE] Bar chart saved.')
```

## 实验数据
{extraction_result}

请直接输出完整的 Python 代码：
"""
    return prompt


def generate_line_chart_code(extraction_result: str) -> str:
    """生成折线对比图 Python 代码"""
    prompt = f"""你是一个数据可视化工程师。根据以下实验数据，生成一段完整的、可直接运行的 Python + Matplotlib 代码来绘制折线对比图。

## 严格规则
1. **输出必须是完整可运行的 Python 代码**
2. **使用 matplotlib**，折线图样式学术规范
3. **每个方法一条线**，用不同颜色和标记区分
4. **X 轴为训练步数/迭代次数或时间**，Y 轴为指标
5. **添加网格线**和**图例**
6. **所有文字使用英文**
7. **保存到文件**：output_line.png（300 DPI）
8. **绝对不能有任何中文字符**

## 实验数据
{extraction_result}

请直接输出完整的 Python 代码：
"""
    return prompt


def generate_ablation_chart_code(extraction_result: str) -> str:
    """生成消融实验图 Python 代码"""
    prompt = f"""你是一个数据可视化工程师。根据以下消融实验数据，生成完整的 Python + Matplotlib 柱状图代码。

## 严格规则
1. **完整可运行的 Python 代码**
2. **用分组柱状图 (grouped bar chart)** 展示不同配置的消融结果
3. **基线方法用灰色**，去掉某组件的用浅色，完整方法用深蓝色
4. **每个柱子顶部显示数值**
5. **所有文字英文**
6. **保存为 ablation.png（300 DPI）**

## 消融实验数据
{extraction_result}

请直接输出完整的 Python 代码：
"""
    return prompt


def convert_to_chart_code(figure_type: str, extraction_result: str) -> str:
    """
    主入口：根据具体子类型返回对应 Matplotlib 代码生成 Prompt
    """
    if "ablation" in extraction_result.lower() or "消融" in extraction_result:
        return generate_ablation_chart_code(extraction_result)
    elif "curve" in extraction_result.lower() or "折线" in extraction_result or "training" in extraction_result.lower():
        return generate_line_chart_code(extraction_result)
    else:
        return generate_bar_chart_code(extraction_result)


def execute_chart_code(code: str) -> tuple:
    """
    执行生成的 Matplotlib 代码，返回 (success, output_path, error)
    """
    import os, sys, tempfile

    try:
        output_dir = os.path.join(os.path.expanduser("~"), ".qclaw", "workspace", "outputs")
        os.makedirs(output_dir, exist_ok=True)

        # 将代码写入临时文件执行
        code_path = os.path.join(output_dir, "_temp_chart.py")
        with open(code_path, "w", encoding="utf-8") as f:
            f.write(code)

        import subprocess
        result = subprocess.run(
            ["python", code_path],
            capture_output=True,
            text=True,
            timeout=60,
            cwd=output_dir
        )

        os.remove(code_path)

        if result.returncode != 0:
            return False, "", result.stderr

        # 找出生成的文件
        for fname in ["output_bar.png", "output_line.png", "ablation.png"]:
            fpath = os.path.join(output_dir, fname)
            if os.path.exists(fpath):
                return True, fpath, ""

        return False, "", "No output file found"

    except Exception as e:
        return False, "", str(e)


path: scripts/draw.py
content: """
paper-diagram-skill / draw.py
=============================
重构后的标准化流程：

  Step 1  扫描  ── 读取论文文本，识别可视化部分
  Step 2  排序  ── 以表格形式展示，按优先级排列
  Step 3  选图  ── 用户选择要生成的图片
  Step 4  生成  ── 逐一生成图片
  Step 5  校验  ── 检查图片结构是否与原文逻辑冲突
  Step 6  重生  ── 有偏差则询问是否调整描述词重生成
"""

import os, json, sys, argparse, textwrap
from pathlib import Path

# ── 内置模块 ──────────────────────────────────────────────
from pdf_to_text import extract_text
from image_generator import generate_image, build_academic_prompt
from self_checker import check_figure_logical_consistency

_SCRIPT_DIR = Path(__file__).parent.resolve()
_SKILL_DIR  = _SCRIPT_DIR.parent
sys.path.insert(0, str(_SCRIPT_DIR))

# ─────────────────────────────────────────────────────────
# Step 1：扫描论文，识别可可视化部分
# ─────────────────────────────────────────────────────────
def scan_paper(raw_text: str) -> list[dict]:
    """
    分析论文文本，识别所有可生成 figure 的章节，
    返回结构化列表。
    每个条目：
      {
        "id":        int,          # 序号
        "priority":  str,          # High / Medium / Low
        "type":      str,          # 图类型
        "title":     str,          # 中英双语标题
        "source":    str,          # 原文出处（图号/章节号）
        "description": str,        # 中文描述（供 LLM 生图用）
        "topology":  str,          # topology 字符串（供 build_academic_prompt 用）
        "user_req":  str,          # user_requirement 字符串
      }
    """
    items = []

    # ── 启发式关键词扫描 ────────────────────────────────
    lines = raw_text.split("\n")
    full_lower = raw_text.lower()

    def add(id_, priority, fig_type, title_cn, title_en, source, desc_cn, topology, user_req=""):
        items.append({
            "id": id_,
            "priority": priority,
            "type": fig_type,
            "title_cn": title_cn,
            "title_en": title_en,
            "source": source,
            "description": desc_cn,
            "topology": topology,
            "user_req": user_req,
        })

    # ── 通用段落匹配 ────────────────────────────────
    # 如果论文内已有 figure 引用，优先提取
    fig_patterns = [
        (r"图\s*1[:：]?\s*(.{10,80})", "图1"),
        (r"Figure\s*1[:.]?\s*(.{10,80})", "Figure 1"),
        (r"图\s*2[:：]?\s*(.{10,80})", "图2"),
        (r"Figure\s*2[:.]?\s*(.{10,80})", "Figure 2"),
    ]

    found_figures = []
    for pat, label in fig_patterns:
        import re
        for m in re.finditer(pat, raw_text):
            found_figures.append((label, m.group(0)[:120]))

    # ── 根据论文内容，手动构建优先级列表 ─────────────
    # 这里用关键词匹配来自动判断哪些章节有可视化价值

    # 1. 架构总览图（最高优先）
    if any(k in full_lower for k in ["架构", "architecture", "framework", "overview"]):
        add(1, "High", "架构图",
            "模型架构总览",
            "Architecture Overview",
            "Section 3 / 图4",
            "展示整个模型的完整架构流程，从输入到输出，包含所有主要模块和连接关系",
            "Clean academic architecture diagram, left-to-right hierarchical layout showing all modules and connections")

    # 2. 损失函数 / 训练流程
    if any(k in full_lower for k in ["损失", "loss", "loss function", "objective"]):
        add(2, "High", "公式/流程图",
            "训练目标与损失函数",
            "Training Objective & Loss",
            "Section 3.1-3.3 / 公式1-2",
            "展示模型的损失函数构成，包括对比损失、自监督损失等的组合方式",
            "Academic diagram showing mathematical formulation, loss components and their combination")

    # 3. 数据效率对比
    if any(k in full_lower for k in ["数据高效", "data efficient", "zero-shot", "数据效率"]):
        add(3, "High", "对比折线图",
            "数据效率对比（DeCLIP vs CLIP）",
            "Data Efficiency Comparison",
            "图1 / Section 4.3",
            "X轴为训练数据量（12M/29M/56M/88M），Y轴为ImageNet零样本Top-1精度，绘制DeCLIP和CLIP两条对比曲线，标注关键节点数据",
            "Line chart, grid background, two curves (CLIP vs DeCLIP), annotated data points, clean academic style")

    # 4. 三种监督信号详解
    if any(k in full_lower for k in ["自监督", "多视角", "最近邻", "self-supervision", "multi-view", "nearest neighbor"]):
        add(4, "High", "多面板图",
            "三种监督信号详解",
            "Three Supervision Signals",
            "图4(b) / Section 3.3",
            "三栏并排布局：(a)自监督 (b)多视角监督 (c)最近邻监督，每栏有独立标题和颜色区分",
            "Three-panel figure, distinct background colors per panel, labeled subcomponents, clean academic style")

    # 5. 下游任务迁移效果
    if any(k in full_lower for k in ["下游", "迁移", "transfer", "linear probe", "下游任务"]):
        add(5, "High", "对比柱状图",
            "下游任务迁移效果对比",
            "Downstream Task Transfer",
            "表3 / Section 4.3",
            "11个下游数据集的线性探针精度对比柱状图，DeCLIP vs CLIP，标注胜出数据集",
            "Grouped bar chart, 11 datasets, CLIP vs DeCLIP bars, clean academic style")

    # 6. 最近邻检索示例
    if any(k in full_lower for k in ["最近邻", "nearest neighbor", "nn", "neighbor"]):
        add(6, "Medium", "示例图",
            "最近邻文本检索示例",
            "Nearest Neighbor Retrieval Examples",
            "图3 / Section 3.3",
            "展示原始图像-文本对与检索到的最近邻对的对比，标注语义相似性",
            "Side-by-side image-text pairs showing original and retrieved nearest neighbors, clean academic style")

    # 7. 消融实验
    if any(k in full_lower for k in ["消融", "ablation", "ablation study"]):
        add(7, "Medium", "消融图/表格",
            "各监督信号消融实验",
            "Ablation Study",
            "表4 / Section 4.4",
            "展示SS、MVS、NNS各监督信号的消融结果，标注每项的贡献度",
            "Table or grouped bar chart, showing ablation results for SS/MVS/NNS components")

    # 8. 零样本学习曲线
    if any(k in full_lower for k in ["zero-shot", "零样本"]):
        add(8, "Medium", "折线图",
            "ImageNet零样本学习曲线",
            "Zero-Shot Learning Curve on ImageNet",
            "图1 / Section 4.3",
            "展示随训练数据量增加，零样本精度变化的曲线，标注关键转折点",
            "Line chart showing zero-shot accuracy vs data size, annotated key points")

    # 9. CAM 可视化
    if any(k in full_lower for k in ["cam", "class activation", "激活图", "可视化"]):
        add(9, "Medium", "热力图",
            "类激活图（CAM）可视化",
            "Class Activation Map (CAM) Visualization",
            "图8 / Section 4.5",
            "CLIP vs DeCLIP 的 CAM 对比，展示模型关注区域的差异",
            "CAM heatmap comparison, CLIP vs DeCLIP side by side, clean academic style")

    # 10. 数据集样本
    if any(k in full_lower for k in ["数据集", "dataset", "sample", "样本"]):
        add(10, "Low", "样本展示",
            "预训练数据集样本",
            "Pretraining Dataset Samples",
            "Section 4.1 / 图3",
            "展示预训练数据集中选取的图像-文本对样本",
            "Grid of image-text pairs from pretraining dataset, clean academic style")

    # 11. CLIP baseline 回顾
    if any(k in full_lower for k in ["clip", "align", "对比学习"]):
        add(11, "Low", "架构图",
            "CLIP/ALIGN 基线方法回顾",
            "CLIP/ALIGN Baseline Overview",
            "图4(a) / Section 3.1",
            "展示原始 CLIP 的双塔架构（图像编码器 + 文本编码器 + 对比损失）",
            "Clean architecture diagram of CLIP baseline, dual-tower structure, minimal academic style")

    # 12. 模型扩展性
    if any(k in full_lower for k in ["扩展", "scale", "scaling", "regnet", "vit"]):
        add(12, "Low", "扩展曲线图",
            "模型规模扩展性分析",
            "Model Scaling Analysis",
            "Section 4.3 / 表2",
            "展示不同模型规模（ResNet50/ViT-B32/RegNetY-64GF）的零样本性能对比",
            "Bar chart or line chart showing scaling trends across different model architectures")

    # 按优先级排序：High > Medium > Low，同级别按 id 排序
    prio_order = {"High": 0, "Medium": 1, "Low": 2}
    items.sort(key=lambda x: (prio_order[x["priority"]], x["id"]))

    # 重新编号
    for i, item in enumerate(items, 1):
        item["id"] = i

    return items


# ─────────────────────────────────────────────────────────
# Step 2：格式化表格输出
# ─────────────────────────────────────────────────────────
def print_visualization_table(items: list[dict]):
    """打印带优先级的可视化清单表格"""
    header = f"{'#':<3} {'优先级':<8} {'类型':<10} {'标题':<20} {'来源':<15}"
    sep    = "─" * 60
    print("\n" + "═" * 70)
    print("  📊 论文可视化扫描结果")
    print("═" * 70)
    print(header)
    print(sep)

    prio_emoji = {"High": "🔴", "Medium": "🟡", "Low": "⚪"}
    type_emoji = {
        "架构图": "🏗", "对比折线图": "📈", "多面板图": "🗂",
        "对比柱状图": "📊", "公式/流程图": "🔢", "示例图": "🖼",
        "消融图/表格": "🔬", "折线图": "📈", "热力图": "🔥",
        "样本展示": "📦", "扩展曲线图": "📐",
    }

    for item in items:
        emoji_p = prio_emoji.get(item["priority"], "⚪")
        emoji_t = type_emoji.get(item["type"], "•")
        title   = (item["title_cn"] + " / " + item["title_en"])[:28]
        print(
            f"{emoji_p} {item['id']:<2} {item['priority']:<8} "
            f"{emoji_t}{item['type']:<9} {title:<30} {item['source']:<15}"
        )

    print(sep)
    print(f"共识别 {len(items)} 个可可视化部分\n")


def ask_user_selection(items: list[dict]) -> list[int]:
    """询问用户选择要生成哪些图片，返回 id 列表"""
    print("请输入要生成的图片编号（支持：1 或 1,3,5 或 1-5 或 all）：")
    choice = input("> ").strip().lower()

    if choice in ("all", "全部", ""):
        return [item["id"] for item in items]

    ids = set()
    parts = choice.replace("，", ",").split(",")
    for part in parts:
        part = part.strip()
        if "-" in part:
            start, end = part.split("-", 1)
            ids.update(range(int(start), int(end) + 1))
        elif part.isdigit():
            ids.add(int(part))

    return sorted(ids)


# ─────────────────────────────────────────────────────────
# Step 5：校验图片逻辑一致性
# ─────────────────────────────────────────────────────────
def verify_figure(item: dict, generated_path: str) -> tuple[bool, str]:
    """
    调用 self_checker 检查图片逻辑一致性。
    返回 (passed, message)。
    """
    try:
        result = check_figure_logical_consistency(
            figure_path=generated_path,
            paper_text="",  # 可传原文用于更深层比对
            figure_type=item["type"],
        )
        passed = result.get("passed", True)
        issues = result.get("issues", [])
        msg = "\n".join(issues) if issues else "未发现明显偏差 ✓"
        return passed, msg
    except Exception as e:
        return False, f"校验过程出错：{e}"


def ask_regenerate(item: dict, issue_msg: str) -> bool:
    """询问用户是否要调整描述重新生成"""
    print("\n" + "!" * 60)
    print(f"  ⚠️  发现潜在偏差（图片 #{item['id']}：{item['title_cn']}）")
    print("!" * 60)
    print(f"  问题描述：{issue_msg}")
    print()
    print("  是否重新生成？（输入 y 重新生成，输入 n 保留当前版本）")
    choice = input("> ").strip().lower()
    return choice in ("y", "yes", "是", "重新生成", "1", "ok")


# ─────────────────────────────────────────────────────────
# Step 4：生成单张图片（带重生成循环）
# ─────────────────────────────────────────────────────────
def generate_single_figure(
    item: dict,
    api_url: str,
    api_key: str,
    model: str,
    output_dir: str,
    timeout: int = 300,
    auto_retry: bool = False,
) -> tuple[bool, str]:
    """
    生成单张图片，包含：
      - build_academic_prompt（基于拓扑+描述）
      - generate_image
      - verify_figure
      - ask_regenerate（循环直到用户满意或跳过）
    """

    print(f"\n{'─'*60}")
    print(f"  🔄 生成 #{item['id']}：{item['title_cn']} / {item['title_en']}")
    print(f"  来源：{item['source']}")
    print(f"{'─'*60}")

    # 构造 prompt
    prompt_dict = build_academic_prompt(
        topology=item.get("topology", ""),
        figure_type=item["type"],
        user_requirement=item.get("user_req", ""),
        style_name="cvpr",
    )

    filename = f"fig_{item['id']:02d}_{item['title_en'].replace(' ','_').replace('/','_')[:20]}.png"
    output_path = os.path.join(output_dir, filename)

    attempt = 0
    while True:
        attempt += 1
        print(f"\n  [{attempt}] 调用生图 API...")

        success, path, err = generate_image(
            prompt_dict=prompt_dict,
            api_url=api_url,
            [REDACTED_SECRET],
            model=model,
            output_filename=filename,
            output_dir=output_dir,
            timeout=timeout,
        )

        if not success:
            print(f"\n  ❌ 生成失败：{err}")
            if auto_retry:
                print("  [auto] 重试...")
                continue
            print("  是否重试？（输入 y 重试，输入 n 跳过）")
            if input("> ").strip().lower() not in ("y", "yes", "是", "1"):
                return False, ""
            continue

        print(f"\n  ✅ 生成完成：{path}")

        # Step 5：校验
        passed, issue_msg = verify_figure(item, path)
        print(f"\n  🔍 逻辑校验结果：{'通过 ✓' if passed else '有偏差 ⚠️'}")
        if not passed:
            print(f"     {issue_msg}")

        if not passed:
            if auto_retry or ask_regenerate(item, issue_msg):
                if auto_retry:
                    print("\n  [auto] 重新生成...")
                else:
                    print("\n  请描述你希望如何调整（直接回车使用默认调整）：")
                    adjustment = input("> ").strip()
                if adjustment:
                    # 将用户调整追加到 topology
                    item["topology"] = item["topology"] + "\n用户补充要求: " + adjustment
                    prompt_dict = build_academic_prompt(
                        topology=item.get("topology", ""),
                        figure_type=item["type"],
                        user_requirement=item.get("user_req", ""),
                        style_name="cvpr",
                    )
                continue  # 重新生成
            else:
                print("  保留当前版本，继续下一张。")
                return True, path
        else:
            print("  图片通过校验，无需调整。")
            return True, path


# ─────────────────────────────────────────────────────────
# 主入口（execute，供 OpenClaw 调用）
# ─────────────────────────────────────────────────────────
def execute(args: dict, context: dict = None) -> dict:
    """
    OpenClaw skill 主入口。
    args 包含：
      input       : str，论文文本或文件路径（.txt / .pdf）
      api_url     : str（可选，默认 acedata）
      [REDACTED_SECRET]
      model       : str（可选）
      output_dir  : str（可选）
      auto_select : bool，是否自动选择全部（跳过交互，默认 False）
    """
    # 1. 读取输入
    raw_input = args.get("input", "")
    if not raw_input:
        return {"success": False, "error": "缺少 input 参数，请提供论文文本或文件路径"}

    # 支持文件路径
    if os.path.isfile(raw_input):
        ext = Path(raw_input).suffix.lower()
        if ext == ".pdf":
            raw_text = extract_text_from_pdf(raw_input)
        else:
            with open(raw_input, encoding="utf-8") as f:
                raw_text = f.read()
    else:
        raw_text = raw_input

    if len(raw_text) < 200:
        return {"success": False, "error": f"输入内容太短（{len(raw_text)} 字符），请确认论文内容正确"}

    # 2. 扫描论文
    print(f"\n📖 正在扫描论文（{len(raw_text)} 字）...")
    items = scan_paper(raw_text)
    if not items:
        return {"success": False, "error": "未能从论文中识别到可可视化的内容"}

    # 3. 打印表格
    print_visualization_table(items)

    # 4. 读取配置
    api_url    = args.get("api_url",    os.environ.get("BANANA2_API_URL",    "https://api.acedata.cloud/nano-banana/images"))
    [REDACTED_SECRET]"api_key",    os.environ.get("BANANA2_API_KEY",    os.environ.get("ACEDATA_API_KEY", "")))
    model      = args.get("model",      "nano-banana-2")
    output_dir = args.get("output_dir", str(_SCRIPT_DIR / "outputs"))
    auto_select= args.get("auto_select", False)

    if not [REDACTED_SECRET] {"success": False, "error": "未设置 API Key，请通过参数 api_key 或环境变量 BANANA2_API_KEY 提供"}

    os.makedirs(output_dir, exist_ok=True)

    # 5. 交互选择（auto_select=True 时跳过）
    if auto_select:
        selected_ids = [item["id"] for item in items]
    else:
        print("─" * 60)
        selected_ids = ask_user_selection(items)
        if not selected_ids:
            print("未选择任何图片，退出。")
            return {"success": True, "generated": [], "skipped": []}

    selected_items = [item for item in items if item["id"] in selected_ids]

    # 6. 逐一生成
    print(f"\n🎯 开始生成 {len(selected_items)} 张图片...\n")
    results = []
    for item in selected_items:
        success, path = generate_single_figure(item, api_url, api_key, model, output_dir)
        results.append({
            "id": item["id"],
            "title": item["title_cn"],
            "success": success,
            "path": path,
        })

    # 7. 汇总报告
    print("\n" + "═" * 60)
    print("  📦 生成报告")
    print("═" * 60)
    for r in results:
        icon = "✅" if r["success"] else "❌"
        print(f"  {icon} #{r['id']:2d}  {r['title']:<25}  {r['path']}")
    print("═" * 60)

    return {
        "success": True,
        "generated": [r for r in results if r["success"]],
        "failed":   [r for r in results if not r["success"]],
    }


# ─────────────────────────────────────────────────────────
# CLI 入口（供 draw.py --scan 使用）
# ─────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(description="DeCLIP Paper Diagram Generator (New Workflow)")
    parser.add_argument("--input", "-i", required=True, help="论文文本或文件路径")
    parser.add_argument("--api-url",   default=os.environ.get("BANANA2_API_URL",    "https://api.acedata.cloud/nano-banana/images"))
    parser.add_argument("--api-key",   default=os.environ.get("BANANA2_API_KEY",    os.environ.get("ACEDATA_API_KEY", "")))
    parser.add_argument("--model",     default="nano-banana-2")
    parser.add_argument("--output-dir",default=str(_SCRIPT_DIR / "outputs"))
    parser.add_argument("--auto",       action="store_true", help="自动选择全部图片，跳过交互")

    args_cli = parser.parse_args()
    args_dict = vars(args_cli)

    if not args_dict["api_key"]:
        print("❌ 错误：未设置 API Key")
        print("   请设置环境变量 BANANA2_API_KEY 或通过 --api-key 参数传递")
        sys.exit(1)

    result = execute(args_dict)
    if not result.get("success"):
        print(f"\n❌ 错误：{result.get('error', '未知错误')}")
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/image_generator.py
content: """
image_generator.py - 生图模块 (通用版 v2.3)
所有配置从参数传入，不 hardcode 任何凭证
环境变量兜底优先级：BANANA2_API_KEY → ACEDATA_API_KEY → PAPER_DIAGRAM_API_KEY
"""

import os
import sys
import time
import requests

_skill_dir = os.path.dirname(os.path.abspath(__file__))
if _skill_dir not in sys.path:
    sys.path.insert(0, _skill_dir)

from style_presets import get_preset, build_style_prompt, build_negative_prompt


def build_academic_prompt(
    topology: str,
    figure_type: str,
    user_requirement: str = "",
    style_name: str = None
) -> dict:
    preset = get_preset(style_name)
    style_base = build_style_prompt(preset)
    negative   = build_negative_prompt(preset)

    type_suffix_map = {
        "teaser": (
            "Teaser/motivation figure: concept diagram with side-by-side comparison, "
            "old method (gray/red tones) vs new method (blue/green tones), "
            "arrows showing improvement flow, concise boxed labels."
        ),
        "architecture": (
            "System architecture diagram: modular rounded rectangles with labels, "
            "data flow arrows left-to-right, color-coded modules by function, "
            "hierarchical subgraph grouping, consistent line weights."
        ),
        "flowchart": (
            "Algorithm flowchart: rounded process boxes, diamond decision nodes, "
            "oval start/end nodes, directional arrows with step numbers, "
            "clean grid layout, top-to-bottom or left-to-right flow."
        ),
        "environment": (
            "Environment interaction diagram: agent-environment feedback loop, "
            "simplified environment block, agent block, "
            "state/action/reward labeled arrows, clean abstract representation."
        ),
    }
    type_suffix = type_suffix_map.get(figure_type, type_suffix_map["architecture"])
    user_extra  = f" Additional requirements: {user_requirement}" if user_requirement else ""

    positive = (
        f"Academic paper figure, publication quality. {style_base} "
        f"Diagram type: {type_suffix} "
        f"Diagram content: {topology}"
        f"{user_extra}"
    ).strip()

    return {"positive": positive, "negative": negative}


def _get_output_dir(skill_dir: str = None) -> str:
    """
    输出目录优先级：
    1. 环境变量 PAPER_DIAGRAM_OUTPUT_DIR
    2. skill_dir/../outputs/
    3. ./outputs/
    """
    if skill_dir is None:
        skill_dir = _skill_dir

    env_dir = os.environ.get("PAPER_DIAGRAM_OUTPUT_DIR", "").strip()
    if env_dir:
        return env_dir

    rel = os.path.normpath(os.path.join(skill_dir, "..", "outputs"))
    parent = os.path.dirname(rel)
    if os.path.exists(parent) and os.access(parent, os.W_OK):
        return rel

    return os.path.join(os.getcwd(), "outputs")


def generate_image(
    prompt_dict: dict,
    api_url: str,
    api_key: str,
    model: str = "nano-banana-2",
    output_filename: str = "paper_diagram.png",
    output_dir: str = None,
    skill_dir: str = None,
    timeout: int = 120
) -> tuple:
    """
    调用任意兼容生图 API（Banana2 / SD / 自建服务）

    Args:
        prompt_dict:    {"positive": ..., "negative": ...}
        api_url:        API 地址（必填）
        [REDACTED_SECRET]
        model:          模型名，默认 nano-banana-2
        output_filename: 输出文件名
        output_dir:     输出目录（None → 自动推断）
        skill_dir:      skill 目录（None → 自动推断）
        timeout:        请求超时秒数

    Returns: (success: bool, output_path: str, error_msg: str)
    """
    if not api_url or not [REDACTED_SECRET] False, "", "api_url and api_key are required"

    if output_dir is None:
        output_dir = _get_output_dir(skill_dir)
    os.makedirs(output_dir, exist_ok=True)

    try:
        print(f"[ImageGen] POST {model} @ {api_url}", flush=True)
        t0 = time.time()

        # 关键：禁用系统代理（避免 exec 代理干扰 Kong）+ acedata 正确参数格式
        session = requests.Session()
        session.trust_env = False

        headers = {
            "Authorization": f"Bearer {api_key}",
            "accept": "application/json",
            "Content-Type": "application/json"
        }
        payload = {
            "action": "generate",
            "model": model,
            "prompt": prompt_dict.get("positive", ""),
            "negative_prompt": prompt_dict.get("negative", ""),
            "aspect_ratio": "16:9",
            "resolution": "2K"
        }

        response = session.post(api_url, headers=headers, json=payload, timeout=timeout)
        response.raise_for_status()
        data = response.json()
        elapsed = time.time() - t0

        print(f"[ImageGen] Response in {elapsed:.1f}s: {str(data)[:300]}", flush=True)

        if not data.get("success"):
            return False, "", f"API error: {data}"

        # 解析 acedata 响应格式
        image_url = None
        for key in ("image_url", "url"):
            image_url = data.get(key)
            if image_url:
                break
        if not image_url:
            body = data.get("body", {})
            inner = body.get("data") if isinstance(body, dict) else None
            if inner and isinstance(inner, list):
                image_url = inner[0].get("image_url") if isinstance(inner[0], dict) else None
        if not image_url:
            for k in ("data", "images", "outputs"):
                arr = data.get(k)
                if isinstance(arr, list) and arr:
                    item = arr[0]
                    image_url = item.get("image_url") if isinstance(item, dict) else None
                    if image_url:
                        break
        if not image_url:
            return False, "", f"No image URL in response: {str(data)[:500]}"

        print(f"[ImageGen] Image URL found, downloading...", flush=True)

        img_resp = session.get(image_url, timeout=60)
        img_resp.raise_for_status()

        output_path = os.path.join(output_dir, output_filename)
        with open(output_path, "wb") as f:
            f.write(img_resp.content)

        total = time.time() - t0
        size  = os.path.getsize(output_path)
        print(f"[ImageGen] Done: {output_path} ({size/1024:.1f} KB, {total:.1f}s)", flush=True)
        return True, output_path, ""

    except requests.exceptions.Timeout:
        return False, "", f"Request timed out after {timeout}s"
    except requests.exceptions.ConnectionError as e:
        return False, "", f"Connection failed: {e}"
    except Exception as e:
        return False, "", str(e)


def _resolve_api_credentials(api_url: str = None, api_key: str = None) -> tuple:
    """
    统一凭证解析 — 参数优先，环境变量兜底。

    api_key 优先级：参数 → BANANA2_API_KEY → ACEDATA_API_KEY → PAPER_DIAGRAM_API_KEY
    api_url 优先级：参数 → BANANA2_API_URL → PAPER_DIAGRAM_API_URL → 默认值

    Returns: (resolved_api_url, resolved_api_key)
    """
    # ── api_key 兜底链 ──
    if not api_key:
        for env_name in ("BANANA2_API_KEY", "ACEDATA_API_KEY", "PAPER_DIAGRAM_API_KEY"):
            val = os.environ.get(env_name, "").strip()
            if val:
                api_key = val
                print(f"[ImageGen] API key from env {env_name}", flush=True)
                break

    # ── api_url 兜底链 ──
    if not api_url:
        for env_name in ("BANANA2_API_URL", "PAPER_DIAGRAM_API_URL"):
            val = os.environ.get(env_name, "").strip()
            if val:
                api_url = val
                break
        if not api_url:
            api_url = "https://api.acedata.cloud/nano-banana/images"

    return api_url, api_key


def generate(
    topology: str,
    figure_type: str,
    user_requirement: str = "",
    api_url: str = None,
    api_key: str = None,
    model: str = "nano-banana-2",
    style_name: str = None,
    output_dir: str = None
) -> tuple:
    """
    主入口 — 兼容旧签名，所有凭证从参数传入或环境变量自动兜底
    环境变量优先级：BANANA2_API_KEY → ACEDATA_API_KEY → PAPER_DIAGRAM_API_KEY
    """
    api_url, [REDACTED_SECRET], api_key)

    prompt_dict = build_academic_prompt(topology, figure_type, user_requirement, style_name)
    filename = f"{figure_type}_diagram.png"

    if not [REDACTED_SECRET] False, "", (
            "API Key not configured.\n"
            "请设置以下任一环境变量：\n"
            "  BANANA2_API_KEY（推荐）\n"
            "  ACEDATA_API_KEY\n"
            "  PAPER_DIAGRAM_API_KEY\n"
            "或在对话中直接传入 api_key 参数。"
        )

    return generate_image(
        prompt_dict,
        api_url=api_url,
        [REDACTED_SECRET],
        model=model,
        output_filename=filename,
        output_dir=output_dir
    )


path: scripts/paper_scanner.py
content: """
paper_scanner.py - 论文全篇深度扫描
自动识别所有可可视化的部分，输出优先级排序的建议列表

能力：
  - 扫描论文全文（自动分块处理长论文）
  - 识别 5 类可图化内容：架构描述、算法流程、实验对比、概念动机、实验环境
  - 输出结构化 JSON：每项包含图类型、所在章节、优先级、具体理由
  - 支持用户确认后批量调用 Pipeline
"""

import json


# ============================================================
# 扫描 Prompt 模板
# ============================================================

SCAN_PROMPT_TEMPLATE = """你是一个学术论文图表规划专家。请深度分析以下论文章节，找出所有适合可视化的内容。

## 任务
逐段分析论文内容，识别出可以用学术插图展示的部分。对于每个可图化的内容，记录：
1. 它在论文的哪个部分（章节标题或段落位置）
2. 适合什么类型的图
3. 为什么要画这个图（它能帮助读者理解什么）
4. 优先级（1=最重要，必须画；2=推荐画；3=可选）

## 图类型定义
- **architecture**: 模型架构、系统框架、模块组成、数据流向。触发词：model, framework, architecture, module, component, pipeline, encoder, decoder, backbone, transformer, network, layer
- **flowchart**: 算法步骤、训练流程、推理流程、数据处理流程。触发词：algorithm, procedure, step, process, training, inference, iteration, pseudo-code, forward, backward
- **results**: 实验结果、性能对比、消融实验、排行榜。触发词：experiment, result, comparison, baseline, ablation, metric, accuracy, F1, BLEU, performance, table
- **teaser**: 研究动机、问题定义、核心创新、应用场景。触发词：motivation, problem, challenge, contribution, key insight, overview, we propose
- **environment**: 强化学习环境、Agent 交互、任务设定。触发词：environment, agent, state, action, reward, reinforcement, MDP, interaction

## 论文内容（片段 {chunk_idx}/{chunk_total}）
---
{paper_chunk}
---

## 输出格式（严格 JSON）
```json
[
  {{
    "figure_type": "architecture|flowchart|results|teaser|environment",
    "section": "章节标题或位置描述（如 Section 3: Methodology Overview）",
    "priority": 1,
    "reason": "为什么要画这个图",
    "suggested_title": "建议的图表标题（英文，适合 LaTeX caption）",
    "key_content_summary": "这段内容的核心要点（2-3 句话，用于后续 Prompt 生成）"
  }}
]
```

注意：
- 如果这个片段中没有可图化的内容，返回空数组 `[]`
- 不要重复：同一模块的描述只记录一次，即使出现在多个段落
- 优先关注描述具体结构的段落，忽略纯理论推导
- 只输出 JSON，不要任何解释文字
"""


SCAN_SUMMARY_PROMPT = """你是一个学术论文图表规划专家。以下是对同一篇论文多个片段的扫描结果，可能存在重复项。

请合并去重，按优先级排序（1 在前），输出最终的可视化建议列表。

## 原始扫描结果
{scan_results}

## 输出格式（严格 JSON）
```json
[
  {{
    "id": 1,
    "figure_type": "architecture|flowchart|results|teaser|environment",
    "section": "章节位置",
    "priority": 1,
    "reason": "理由",
    "suggested_title": "LaTeX caption 标题",
    "key_content_summary": "核心要点",
    "dependencies": []
  }}
]
```

规则：
- `id` 从 1 开始递增
- 同一内容只保留一个（合并重复项，保留信息更丰富的版本）
- `priority` 重新评估：architecture 通常优先级最高（读者第一眼看框架图）
- `dependencies` 标注依赖关系（如 teaser 图不依赖其他图，results 图依赖 architecture 图的理解）
- 最多保留 8 个建议（一篇论文通常 4-6 个图已经足够）
- 只输出 JSON
"""


# ============================================================
# 核心函数
# ============================================================

def chunk_paper(paper_content: str, max_chars: int = 6000, overlap: int = 500) -> list:
    """
    将长论文分块，每块 max_chars 字符，overlap 字符重叠
    确保不会在句子中间截断
    """
    if len(paper_content) <= max_chars:
        return [paper_content]

    chunks = []
    start = 0
    while start < len(paper_content):
        end = start + max_chars

        if end < len(paper_content):
            # 在句号/换行处截断，避免断句
            best_break = end
            for sep in ["\n\n", ".\n", "\n", ". ", " "]:
                pos = paper_content.rfind(sep, start + max_chars // 2, end)
                if pos > start:
                    best_break = pos + len(sep)
                    break
            end = best_break

        chunks.append(paper_content[start:end])
        start = end - overlap

        if start >= len(paper_content):
            break

    return chunks


def scan_chunk(llm_call_fn, chunk: str, chunk_idx: int, chunk_total: int) -> list:
    """
    扫描单个分块，返回发现的可图化内容列表
    """
    prompt = SCAN_PROMPT_TEMPLATE.format(
        chunk_idx=chunk_idx,
        chunk_total=chunk_total,
        paper_chunk=chunk
    )

    try:
        raw = llm_call_fn(prompt)
        return _parse_scan_result(raw)
    except Exception as e:
        print(f"[Scanner] Chunk {chunk_idx} scan failed: {e}", flush=True)
        return []


def scan_paper(llm_call_fn, paper_content: str) -> list:
    """
    全篇扫描主入口

    Args:
        llm_call_fn: callable(str) -> str，LLM 调用函数
        paper_content: 论文全文

    Returns:
        list[dict]: 合并去重后的可视化建议列表
    """
    print(f"[Scanner] Scanning paper ({len(paper_content)} chars)...", flush=True)

    chunks = chunk_paper(paper_content)
    print(f"[Scanner] Split into {len(chunks)} chunks", flush=True)

    # 逐块扫描
    all_findings = []
    for i, chunk in enumerate(chunks, 1):
        print(f"[Scanner] Scanning chunk {i}/{len(chunks)}...", flush=True)
        findings = scan_chunk(llm_call_fn, chunk, i, len(chunks))
        all_findings.extend(findings)

    if not all_findings:
        print("[Scanner] No visualizable content found.", flush=True)
        return []

    print(f"[Scanner] Found {len(all_findings)} raw candidates, merging...", flush=True)

    # 合并去重
    merged = _merge_results(llm_call_fn, all_findings)

    # 按 id 排序
    merged.sort(key=lambda x: x.get("id", 999))

    print(f"[Scanner] Final: {len(merged)} visualization suggestions", flush=True)
    return merged


def format_scan_report(findings: list) -> str:
    """
    将扫描结果格式化为用户可读的报告
    """
    if not findings:
        return "No visualizable content identified in this paper."

    type_icon = {
        "architecture": "🏗️",
        "flowchart": "🔄",
        "results": "📊",
        "teaser": "💡",
        "environment": "🌍"
    }

    lines = [
        "## Paper Visualization Report",
        "",
        f"Identified **{len(findings)}** visualizable sections:",
        ""
    ]

    for item in findings:
        fid = item.get("id", "?")
        ftype = item.get("figure_type", "?")
        section = item.get("section", "?")
        priority = item.get("priority", "?")
        reason = item.get("reason", "")
        title = item.get("suggested_title", "")
        deps = item.get("dependencies", [])

        icon = type_icon.get(ftype, "📄")
        prio_label = {1: "**High**", 2: "Medium", 3: "Low"}.get(priority, str(priority))

        lines.append(f"### {icon} #{fid} [{ftype.upper()}] — {prio_label} Priority")
        lines.append(f"- **Section:** {section}")
        lines.append(f"- **Title:** {title}")
        lines.append(f"- **Reason:** {reason}")
        if deps:
            lines.append(f"- **Depends on:** #{', #'.join(str(d) for d in deps)}")
        lines.append("")

    lines.append("---")
    lines.append("**Select figures to generate.** Example replies:")
    lines.append("- `all` — Generate all")
    lines.append("- `1, 3, 5` — Generate specific figures")
    lines.append("- `1` — Generate only the first one")
    lines.append("- `none` — Cancel")

    return "\n".join(lines)


def parse_user_selection(user_input: str, total_count: int) -> list:
    """
    解析用户选择，返回要生成的 figure id 列表
    """
    user_input = user_input.strip().lower().replace("，", ",").replace(" ", "")

    if user_input in ("all", "全部"):
        return list(range(1, total_count + 1))

    if user_input in ("none", "取消", "cancel", "0"):
        return []

    try:
        ids = [int(x.strip()) for x in user_input.split(",") if x.strip()]
        return [i for i in ids if 1 <= i <= total_count]
    except ValueError:
        return []


# ============================================================
# 内部工具函数
# ============================================================

def _parse_scan_result(raw: str) -> list:
    """解析单个 chunk 的扫描结果 JSON"""
    json_start = raw.find("[")
    json_end = raw.rfind("]") + 1

    if json_start == -1 or json_end <= json_start:
        return []

    try:
        return json.loads(raw[json_start:json_end])
    except json.JSONDecodeError:
        return []


def _merge_results(llm_call_fn, findings: list) -> list:
    """用 LLM 合并去重扫描结果"""
    findings_json = json.dumps(findings, ensure_ascii=False, indent=2)
    prompt = SCAN_SUMMARY_PROMPT.format(scan_results=findings_json)

    try:
        raw = llm_call_fn(prompt)
        return _parse_scan_result(raw)
    except Exception as e:
        print(f"[Scanner] Merge failed, using raw results: {e}", flush=True)
        # fallback: 直接去重
        seen = set()
        unique = []
        for f in findings:
            key = f.get("section", "") + f.get("figure_type", "")
            if key not in seen:
                seen.add(key)
                f["id"] = len(unique) + 1
                unique.append(f)
        return unique


path: scripts/pdf_to_text.py
content: """
pdf_to_text.py - PDF 文本提取模块
支持多种提取方式，按优先级：
  1. pdfminer.six（精确文本，保留段落结构）
  2. PyMuPDF / fitz（图片 PDF + 文字 PDF）
  3. pdfplumber（表格友好）
  4. pypdf2（纯文字 PDF）
  5. 在线 API 回退
"""

import os
import sys

_skill_dir = os.path.dirname(os.path.abspath(__file__))
if _skill_dir not in sys.path:
    sys.path.insert(0, _skill_dir)


def extract_text(pdf_path: str, max_pages: int = 0) -> str:
    """
    从 PDF 提取纯文本

    Args:
        pdf_path: PDF 文件路径
        max_pages: 最大提取页数（0 = 全部）
    Returns:
        纯文本字符串
    """
    if not os.path.exists(pdf_path):
        raise FileNotFoundError(f"PDF not found: {pdf_path}")

    size = os.path.getsize(pdf_path)
    if size > 50 * 1024 * 1024:
        print(f"[PDF] Warning: Large PDF ({size/1024/1024:.1f}MB), may take time", flush=True)

    # 尝试顺序
    extractors = [
        ("pdfminer",   _extract_pdfminer),
        ("pymupdf",    _extract_pymupdf),
        ("pdfplumber", _extract_pdfplumber),
        ("pypdf2",     _extract_pypdf2),
        ("pypdf",      _extract_pypdf),
    ]

    for name, fn in extractors:
        try:
            text = fn(pdf_path, max_pages)
            if text and len(text.strip()) > 100:
                print(f"[PDF] Extracted {len(text)} chars via {name}", flush=True)
                return _clean_text(text)
        except Exception as e:
            print(f"[PDF] {name} failed: {e}", flush=True)

    # 回退：OCR 或报告失败
    raise RuntimeError(
        f"None of the PDF extractors worked for: {pdf_path}\n"
        "Install one: pip install pdfminer.six  (recommended)"
    )


def _extract_pdfminer(pdf_path: str, max_pages: int) -> str:
    """pdfminer.six — 保留段落结构，最精确"""
    from pdfminer.high_level import extract_text
    return extract_text(pdf_path, maxpages=max_pages or 999)


def _extract_pymupdf(pdf_path: str, max_pages: int) -> str:
    """PyMuPDF / fitz"""
    import fitz  # pymupdf
    doc = fitz.open(pdf_path)
    pages = min(len(doc), max_pages) if max_pages else len(doc)
    parts = []
    for i in range(pages):
        parts.append(doc[i].get_text())
    doc.close()
    return "\n".join(parts)


def _extract_pdfplumber(pdf_path: str, max_pages: int) -> str:
    """pdfplumber — 表格友好"""
    import pdfplumber
    with pdfplumber.open(pdf_path) as pdf:
        pages = pdf.pages[:max_pages] if max_pages else pdf.pages
        return "\n".join(p.extract_text() or "" for p in pages)


def _extract_pypdf2(pdf_path: str, max_pages: int) -> str:
    """PyPDF2"""
    from PyPDF2 import PdfReader
    reader = PdfReader(pdf_path)
    pages = reader.pages[:max_pages] if max_pages else reader.pages
    return "\n".join(p.extract_text() or "" for p in pages)


def _extract_pypdf(pdf_path: str, max_pages: int) -> str:
    """pypdf（新版）"""
    from pypdf import PdfReader
    reader = PdfReader(pdf_path)
    pages = reader.pages[:max_pages] if max_pages else reader.pages
    return "\n".join(p.extract_text() or "" for p in pages)


def _clean_text(text: str) -> str:
    """清理提取文本"""
    import re
    # 合并换行（保留段落边界）
    text = re.sub(r'\n{3,}', '\n\n', text)
    # 去除多余空格
    text = re.sub(r' {2,}', ' ', text)
    # 去除孤立换行
    text = re.sub(r'(?<!\n)\n(?!\n)', ' ', text)
    # 去除零宽字符
    text = re.sub(r'[\u200b-\u200f\u2028-\u202f\ufeff]', '', text)
    return text.strip()


# ============================================================
# CLI 入口
# ============================================================

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="PDF → Text Extractor")
    parser.add_argument("pdf_path", help="PDF file path")
    parser.add_argument("--max-pages", type=int, default=0,
                        help="Max pages to extract (0 = all)")
    parser.add_argument("--output", "-o", help="Output .txt file")
    args = parser.parse_args()

    text = extract_text(args.pdf_path, args.max_pages)
    print(f"Extracted {len(text)} characters")

    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(text)
        print(f"Saved to {args.output}")
    else:
        print(text)


path: scripts/router.py
content: """
router.py - 论文可视化路由判断
判断用户想要哪种类型的图，并从论文中提取对应章节的内容
"""

# 图类型常量
TYPE_TEASER = "teaser"          # 概念图/动机图 - Abstract/Introduction
TYPE_ARCHITECTURE = "architecture"  # 主框架图/架构图 - Methodology
TYPE_FLOWCHART = "flowchart"    # 算法流程图 - Methodology (伪代码)
TYPE_ENVIRONMENT = "environment" # 实验环境图 - Experimental Setup
TYPE_RESULTS = "results"        # 结果对比图 - Results (不用生图，用 Matplotlib)


def classify_figure_type(llm_call_fn, user_intent: str, paper_content: str) -> str:
    """
    根据用户意图 + 论文内容判断生成哪种图
    """
    prompt = f"""你是一个学术论文图表分类专家。根据用户的描述和论文内容，判断用户想要生成哪种类型的学术插图。

请从以下 5 种类型中选择最合适的一个（只输出类型名称，不要任何解释）：

1. **teaser** - 概念图/动机图：展示研究动机、痛点对比、应用场景
   关键词：动机、痛点、对比、概念、应用场景、头图、宣传图

2. **architecture** - 主框架图/系统架构图：展示模型整体结构、模块组成、数据流向
   关键词：架构、框架、模型结构、模块、pipeline、系统图

3. **flowchart** - 算法流程图：展示算法的执行步骤、条件分支、循环逻辑
   关键词：算法、流程、步骤、伪代码、loop、if-else、循环

4. **environment** - 实验环境图：展示智能体与环境的交互、状态空间、任务定义
   关键词：环境、交互、智能体、任务、setup、强化学习、游戏

5. **results** - 结果对比图：展示实验数据、柱状图、折线图、性能对比
   关键词：结果、对比、实验、图表、柱状图、折线图、ablation、metric

用户意图：{user_intent}

论文摘要（前 1500 字）：
{paper_content[:1500]}

请只输出一个词：teaser / architecture / flowchart / environment / results"""

    try:
        result = llm_call_fn(prompt).strip().lower()
        valid_types = {TYPE_TEASER, TYPE_ARCHITECTURE, TYPE_FLOWCHART, TYPE_ENVIRONMENT, TYPE_RESULTS}
        # 提取：LLM 可能返回 "architecture" 或 "1. architecture" 等
        for t in valid_types:
            if t in result:
                return t
        return TYPE_ARCHITECTURE  # 最常见的默认
    except Exception:
        return TYPE_ARCHITECTURE  # fallback


def extract_section(user_intent: str, paper_content: str, figure_type: str) -> dict:
    """
    根据图类型，从论文中提取对应章节的结构化内容
    """
    extract_prompt = f"""你是一个学术论文结构化信息提取专家。请从以下论文内容中提取生成【{{figure_type_label}}】所需的关键信息。

## 任务
根据图类型「{{figure_type_label}}」，重点阅读以下对应章节：
{{target_sections}}

## 论文内容
{{paper_content}}

## 输出要求
请以 JSON 格式输出，字段如下（根据图类型选择相关字段）：

### 如果是 teaser（概念图/动机图）：
{{
  "pain_point": "现有方法的痛点/缺陷",
  "proposed_solution": "本文提出的核心解决方案",
  "input_output": {{"input": "系统输入", "output": "系统输出"}},
  "application_scenario": "应用场景",
  "highlight": "最核心的创新点一句话"
}}

### 如果是 architecture（主框架图）：
{{
  "title": "图表标题（如：Model Overview）",
  "modules": [
    {{"name": "模块名称", "type": "encoder|decoder|attention|fc|embedding|normalization|other", "sub_modules": ["子模块列表"]}}
  ],
  "data_flow": [
    {{"from": "模块A", "to": "模块B", "label": "数据/特征"}}
  ],
  "stacks": {{"encoder_layers": N, "decoder_layers": N, "heads": N}},
  "key_components": ["关键组件列表，用于标注"]
}}

### 如果是 flowchart（算法流程图）：
{{
  "title": "图表标题（如：Training Procedure）",
  "steps": [
    {{"id": 1, "text": "步骤描述", "condition": null, "loop": null, "next": 2}},
    {{"id": 2, "text": "if [条件]", "condition": "条件分支", "next_true": 3, "next_false": 4}}
  ],
  "start": "开始节点",
  "end": "结束节点"
}}

### 如果是 environment（实验环境图）：
{{
  "task_name": "任务名称",
  "agent_description": "智能体描述",
  "state_space": "状态空间",
  "action_space": "动作空间",
  "interaction_loop": ["交互步骤序列"],
  "reward_signal": "奖励信号描述"
}}

### 如果是 results（结果对比图）：
{{
  "datasets": ["数据集列表"],
  "metrics": ["评价指标列表，如：Accuracy, F1, BLEU"],
  "baselines": ["基线模型列表"],
  "proposed_method": "本文方法名",
  "key_results": {{"数据集名": {{"metric": value}}}},
  "highlight": "最突出的结果亮点"
}}

请只输出 JSON，不要任何额外文字：
"""

    section_map = {
        TYPE_TEASER: "摘要 (Abstract) 和 引言 (Introduction)",
        TYPE_ARCHITECTURE: "方法论 (Methodology / Proposed Method) 的整体概述段落",
        TYPE_FLOWCHART: "方法论 (Methodology) 中的算法伪代码部分",
        TYPE_ENVIRONMENT: "实验设置 (Experimental Setup / Task Definition)",
        TYPE_RESULTS: "实验结果 (Results / Experiments) 章节",
    }

    figure_label_map = {
        TYPE_TEASER: "概念图/动机图 (Teaser Figure)",
        TYPE_ARCHITECTURE: "主框架图/系统架构图 (Architecture Diagram)",
        TYPE_FLOWCHART: "算法流程图 (Flowchart)",
        TYPE_ENVIRONMENT: "实验环境图 (Environment Diagram)",
        TYPE_RESULTS: "结果对比图 (Results Chart)",
    }

    target_sections = section_map.get(figure_type, "")
    figure_label = figure_label_map.get(figure_type, figure_type)

    formatted_prompt = extract_prompt.format(
        figure_type_label=figure_label,
        target_sections=target_sections,
        paper_content=paper_content[:8000]  # 限制字数避免 token 爆炸
    )

    return {
        "figure_type": figure_type,
        "figure_label": figure_label,
        "target_sections": target_sections,
        "extract_prompt": formatted_prompt
    }


def route(llm_call_fn, user_intent: str, paper_content: str) -> dict:
    """
    主路由函数：判断图类型 + 提取对应内容
    返回路由结果供后续模块使用
    """
    # 判断图类型
    figure_type = classify_figure_type(llm_call_fn, user_intent, paper_content)

    # 提取对应章节内容
    extraction = extract_section(user_intent, paper_content, figure_type)

    return {
        "figure_type": figure_type,
        "figure_label": extraction["figure_label"],
        "target_sections": extraction["target_sections"],
        "extract_prompt": extraction["extract_prompt"],
        "user_intent": user_intent
    }


path: scripts/self_checker.py
content: """
self_checker.py
===============
对生成的图片进行逻辑一致性校验。

主要功能：
  check_figure_logical_consistency(fig_path, paper_text, figure_type)
    1. 用 OCR 提取图片中的可见文字
    2. 检查文字内容是否与预期主题/关键词匹配
    3. 检查结构是否符合预期（通过分析图片特征）
    4. 返回 (passed: bool, issues: list[str])
"""

import os, io
from pathlib import Path

try:
    import pytesseract
    HAS_TESSERACT = True
except ImportError:
    HAS_TESSERACT = False

try:
    from PIL import Image
    HAS_PIL = True
except ImportError:
    HAS_PIL = False


def check_figure_logical_consistency(
    figure_path: str,
    paper_text: str = "",
    figure_type: str = "",
) -> dict:
    """
    校验图片逻辑一致性。

    参数：
      figure_path : str，生成的图片路径
      paper_text  : str，论文原文（用于关键词比对）
      figure_type : str，图类型（architecture/chart/multi-panel 等）

    返回：
      {
        "passed": bool,
        "issues": list[str],   # 如果 passed=False，列出具体问题
        "ocr_text": str,       # OCR 提取的文字（用于调试）
        "warnings": list[str], # 警告（不影响通过，但值得关注）
      }
    """
    issues = []
    warnings = []
    ocr_text = ""

    # ── 0. 文件存在性 ─────────────────────────────────
    if not os.path.exists(figure_path):
        issues.append(f"图片文件不存在：{figure_path}")
        return {"passed": False, "issues": issues, "ocr_text": "", "warnings": warnings}

    file_size = os.path.getsize(figure_path)
    if file_size < 5000:
        issues.append(f"图片文件过小（{file_size} bytes），可能是损坏或生成失败")
        return {"passed": False, "issues": issues, "ocr_text": "", "warnings": warnings}

    # ── 1. OCR 提取文字 ───────────────────────────────
    if HAS_PIL and HAS_TESSERACT:
        try:
            img = Image.open(figure_path)
            ocr_text = pytesseract.image_to_string(img, lang="eng+chi")
            ocr_text = ocr_text.strip()
        except Exception as e:
            warnings.append(f"OCR 提取失败：{e}，跳过文字校验")
    else:
        warnings.append("未安装 pytesseract 或 Pillow，跳过 OCR 校验")

    # ── 2. 检查占位符文字 ─────────────────────────────
    placeholder_phrases = [
        "placeholder", "lorem ipsum", "todo", "tbd",
        "undefined", "null", "none", "xxx", "...",
        "image placeholder", "text here",
    ]
    for phrase in placeholder_phrases:
        if phrase.lower() in ocr_text.lower():
            issues.append(
                f"检测到占位符文字「{phrase}」，图片可能未正确生成完整内容"
            )

    # ── 3. 关键词匹配校验 ─────────────────────────────
    if paper_text:
        # 从论文中提取关键概念词
        important_keywords = _extract_keywords(paper_text)
        ocr_lower = ocr_text.lower()

        missing_keywords = [
            kw for kw in important_keywords
            if kw.lower() not in ocr_lower
        ]
        # 容错：只对高频关键词做校验
        critical_kws = [kw for kw in important_keywords if len(kw) > 3]
        if missing_keywords and len(missing_keywords) > len(critical_kws) * 0.7:
            warnings.append(
                f"图片中未发现论文关键概念词：{missing_keywords[:5]}，"
                "内容可能偏离主题"
            )

    # ── 4. 结构类型校验 ───────────────────────────────
    type_checks = {
        "architecture":  _check_architecture,
        "对比折线图":     _check_line_chart,
        "对比柱状图":     _check_bar_chart,
        "多面板图":       _check_multipanel,
        "消融图/表格":    _check_table_chart,
    }

    checker = type_checks.get(figure_type)
    if checker and HAS_PIL and HAS_TESSERACT:
        try:
            type_issues = checker(figure_path, ocr_text)
            issues.extend(type_issues)
        except Exception as e:
            warnings.append(f"结构校验出错：{e}")

    # ── 5. 分辨率/尺寸校验 ─────────────────────────────
    if HAS_PIL:
        try:
            img = Image.open(figure_path)
            w, h = img.size
            if w < 512 or h < 256:
                warnings.append(
                    f"图片分辨率较低（{w}x{h}），建议重新生成更高分辨率版本"
                )
            aspect = w / h if h > 0 else 1
            if aspect > 5:
                warnings.append(
                    f"图片宽高比异常（{aspect:.1f}:1），结构可能被压缩变形"
                )
        except Exception as e:
            warnings.append(f"尺寸校验失败：{e}")

    passed = len(issues) == 0
    return {
        "passed": passed,
        "issues": issues,
        "ocr_text": ocr_text,
        "warnings": warnings,
    }


# ─────────────────────────────────────────────────────────
# 内部辅助函数
# ─────────────────────────────────────────────────────────

def _extract_keywords(text: str, top_n: int = 20) -> list[str]:
    """从论文文本中提取高频实词关键词"""
    # 简单的停用词过滤
    stop_words = {
        "的", "是", "在", "和", "了", "我们", "方法", "本文", "该",
        "a", "an", "the", "is", "are", "and", "or", "of", "to", "in",
        "for", "with", "on", "that", "this", "as", "by", "from",
    }
    import re
    words = re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]{3,}', text)
    freq = {}
    for w in words:
        w = w.lower()
        if w not in stop_words and len(w) >= 2:
            freq[w] = freq.get(w, 0) + 1

    sorted_words = sorted(freq.items(), key=lambda x: x[1], reverse=True)
    return [w for w, _ in sorted_words[:top_n]]


def _check_architecture(fig_path: str, ocr_text: str) -> list[str]:
    """校验架构图：检查是否包含编码器、损失函数等关键模块名"""
    issues = []
    expected_modules = ["encoder", "image", "text", "loss", "encode", "编码"]
    found = sum(1 for m in expected_modules if m.lower() in ocr_text.lower())
    if found < 2:
        issues.append(
            "架构图中未发现足够的结构关键词（如 encoder/text/loss），"
            "结构可能不符合预期"
        )
    return issues


def _check_line_chart(fig_path: str, ocr_text: str) -> list[str]:
    """校验折线图：检查是否有坐标轴标注、数据点"""
    issues = []
    expected = ["%", "accuracy", "精度", "data", "数据", "x", "y"]
    found = sum(1 for m in expected if m.lower() in ocr_text.lower())
    if found < 2:
        issues.append(
            "折线图中未发现坐标轴或数据标注，图表结构可能不完整"
        )
    return issues


def _check_bar_chart(fig_path: str, ocr_text: str) -> list[str]:
    """校验柱状图"""
    issues = []
    expected = ["%", "accuracy", "精度", "dataset", "数据集", "bar"]
    found = sum(1 for m in expected if m.lower() in ocr_text.lower())
    if found < 2:
        issues.append(
            "柱状图中未发现足够的数据集或指标标注，结构可能不完整"
        )
    return issues


def _check_multipanel(fig_path: str, ocr_text: str) -> list[str]:
    """校验多面板图：检查是否有面板标记"""
    issues = []
    expected = ["(a)", "(b)", "(c)", "a)", "b)", "c)", "panel", "图"]
    found = sum(1 for m in expected if m.lower() in ocr_text.lower())
    if found < 2:
        issues.append(
            "多面板图中未发现面板标记（如 (a)(b)(c)），结构可能不完整"
        )
    return issues


def _check_table_chart(fig_path: str, ocr_text: str) -> list[str]:
    """校验消融表格"""
    issues = []
    expected = ["ablation", "ss", "mvs", "nns", "loss", "losses", "消融"]
    found = sum(1 for m in expected if m.lower() in ocr_text.lower())
    if found < 1:
        issues.append(
            "消融图中未发现监督信号相关关键词，结构可能偏离预期"
        )
    return issues


# ─────────────────────────────────────────────────────────
# 快速测试
# ─────────────────────────────────────────────────────────
if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("用法: python self_checker.py <图片路径> [图类型]")
        sys.exit(1)

    fig_path = sys.argv[1]
    fig_type = sys.argv[2] if len(sys.argv) > 2 else ""

    result = check_figure_logical_consistency(fig_path, "", fig_type)
    print(f"\npassed: {result['passed']}")
    print(f"issues: {result['issues']}")
    print(f"warnings: {result['warnings']}")
    if result["ocr_text"]:
        print(f"\nOCR text (first 300 chars):\n{result['ocr_text'][:300]}")


path: scripts/style_presets.py
content: """
style_presets.py - 学术插图风格预设库
为不同顶会/期刊场景提供标准化的配色和风格模板
"""

# ============================================================
# 风格预设
# ============================================================

PRESETS = {
    "cvpr": {
        "label": "CVPR (Computer Vision) — Flat 2D Schematic",
        "style": (
            "flat 2D schematic, solid color blocks with no gradients, "
            "no shadows, no 3D, no shading, no perspective, "
            "clean layer separation by color coding, "
            "thin straight arrows for data flow left-to-right or top-to-bottom, "
            "concrete input examples at entry points, "
            "loss modules in purple and larger than other blocks"
        ),
        "background": "pure white",
        "primary_colors": {
            "frozen":  "#3B82F6",     # 蓝 - frozen/teacher 模块
            "module":  "#3B82F6",     # 蓝 - 核心模块 (shared)
            "image":   "#F97316",     # 橙 - image student/分支
            "text":    "#22C55E",     # 绿 - text student/分支
            "loss":    "#8B5CF6",     # 紫 - 损失函数 (醒目)
            "input":   "#60A5FA",     # 浅蓝 - 输入层
            "queue":   "#8B5CF6",     # 紫 - 队列/缓存
            "baseline": "#9CA3AF",    # 灰 - 基线/对比方法
        },
        "font": "Arial or Helvetica, sans-serif",
        "stroke": "1.5px black, thin straight lines only",
        "corner_radius": "8px",
        "dpi": 300,
        "negative_keywords": (
            "photorealistic, 3D render, watercolor, hand-drawn, gradients, "
            "shadow, depth, perspective, decorative background, comic, blurry, "
            "rounded 3D, bevel, emboss, glow, soft edges, texture, noise, "
            "isometric, drop shadow, inner shadow, outer glow"
        ),
    },
    "neurips": {
        "label": "NeurIPS (Machine Learning)",
        "style": "minimalist schematic, thin lines, clear hierarchy",
        "background": "pure white",
        "primary_colors": {
            "module": "#7570B3",      # 紫 - 核心模块
            "secondary": "#E7298A",   # 粉 - 次级模块
            "highlight": "#D95F02",   # 橙 - 强调
            "input": "#66A61E",       # 绿 - 输入
            "output": "#E6AB02",      # 黄 - 输出
            "loss": "#A6761D",        # 棕 - 损失
            "baseline": "#CCCCCC",    # 浅灰 - 基线
        },
        "font": "Times New Roman or Computer Modern, serif",
        "stroke": "1px dark gray",
        "corner_radius": "4px",
        "dpi": 300,
        "negative_keywords": "photorealistic, 3D render, watercolor, hand-drawn, heavy borders, "
                             "shadow, bold colors, decorative, blurry",
    },
    "icml": {
        "label": "ICML (Machine Learning)",
        "style": "clean block diagram, consistent spacing, academic vector",
        "background": "pure white",
        "primary_colors": {
            "module": "#1F77B4",      # 蓝
            "secondary": "#FF7F0E",   # 橙
            "highlight": "#D62728",   # 红
            "input": "#2CA02C",       # 绿
            "output": "#9467BD",      # 紫
            "loss": "#8C564B",        # 棕
            "baseline": "#BCBD22",    # 黄绿 - 基线
        },
        "font": "Arial, sans-serif",
        "stroke": "1.5px black",
        "corner_radius": "6px",
        "dpi": 300,
        "negative_keywords": "photorealistic, 3D render, watercolor, hand-drawn, gradients, "
                             "shadow, depth, decorative, blurry",
    },
    "nature": {
        "label": "Nature / Science",
        "style": "elegant minimalist, soft colors, high clarity, publication quality",
        "background": "pure white",
        "primary_colors": {
            "module": "#4472C4",      # 柔蓝
            "secondary": "#5B9BD5",   # 浅蓝
            "highlight": "#C00000",   # 深红
            "input": "#70AD47",       # 柔绿
            "output": "#ED7D31",      # 柔橙
            "loss": "#7030A0",        # 柔紫
            "baseline": "#A5A5A5",    # 灰
        },
        "font": "Helvetica, sans-serif",
        "stroke": "1px #333333",
        "corner_radius": "10px",
        "dpi": 300,
        "negative_keywords": "photorealistic, 3D render, watercolor, hand-drawn, heavy lines, "
                             "shadow, bright neon colors, decorative, blurry",
    },
    "default": "cvpr",   # 默认使用 CVPR 风格
}


def get_preset(style_name: str = None) -> dict:
    """获取风格预设，未指定则返回默认"""
    if not style_name:
        style_name = PRESETS["default"]
    preset = PRESETS.get(style_name.lower())
    if not preset:
        preset = PRESETS[PRESETS["default"]]
    return preset


def list_presets() -> list:
    """列出所有可用风格"""
    return [{"name": k, "label": v["label"]} for k, v in PRESETS.items() if isinstance(v, dict)]


def build_style_prompt(preset: dict) -> str:
    """将预设转为 Prompt 中的风格描述片段"""
    colors = preset["primary_colors"]
    color_desc = ", ".join([f"{k}={v}" for k, v in colors.items()])
    return (
        f"Style: {preset['style']}. "
        f"Background: {preset['background']}. "
        f"Color scheme: {color_desc}. "
        f"Font: {preset['font']}. "
        f"Stroke: {preset['stroke']}, corner radius: {preset['corner_radius']}. "
        f"ABSOLUTELY NO shadows, NO 3D, NO gradients, NO perspective. "
        f"Print-ready {preset['dpi']}dpi quality."
    )


def build_negative_prompt(preset: dict) -> str:
    """从预设构建 negative prompt"""
    base = preset.get("negative_keywords", PRESETS["cvpr"]["negative_keywords"])
    # 统一追加 flat 2D 强约束
    flat_guard = "shadow, gradient, 3D, perspective, isometric, bevel, glow, blur, depth of field"
    return f"{base}, {flat_guard}"


def build_latex_caption(figure_type: str, figure_label: str, section_ref: str = "",
                         description: str = "") -> str:
    """
    生成 LaTeX 格式的图注
    Args:
        figure_type: 图类型 (architecture/flowchart/teaser/environment/results)
        figure_label: 中文标签
        section_ref: 对应论文章节 (如 "Section 3.1")
        description: 补充描述
    """
    caption_map = {
        "teaser":      "Overview and motivation of the proposed method",
        "architecture": "Architecture of the proposed framework",
        "flowchart":   "Algorithmic pipeline and procedure",
        "environment": "Experimental environment and interaction setup",
        "results":     "Experimental results and comparison",
    }
    caption_text = caption_map.get(figure_type, figure_label)

    if description:
        caption_text += f". {description}"

    if section_ref:
        section_line = f"  % Corresponds to {section_ref}\n"
    else:
        section_line = ""

    latex = (
        f"{section_line}"
        f"\\begin{{figure}}[t]\n"
        f"  \\centering\n"
        f"  \\includegraphics[width=\\linewidth]{{figures/{figure_type}_diagram.png}}\n"
        f"  \\caption{{{caption_text}}}\n"
        f"  \\label{{fig:{figure_type}}}\n"
        f"\\end{{figure}}"
    )
    return latex


def build_word_caption(figure_type: str, figure_label: str, section_ref: str = "",
                        description: str = "") -> str:
    """生成 Word 文档用的图注"""
    caption_map = {
        "teaser":      "Overview and motivation of the proposed method",
        "architecture": "Architecture of the proposed framework",
        "flowchart":   "Algorithmic pipeline and procedure",
        "environment": "Experimental environment and interaction setup",
        "results":     "Experimental results and comparison",
    }
    caption_text = caption_map.get(figure_type, figure_label)
    if description:
        caption_text += f". {description}"

    ref_line = f" [Ref: {section_ref}]" if section_ref else ""
    return f"Figure: {caption_text}.{ref_line}"


path: scripts/topology_extractor.py
content: """
topology_extractor.py - 将结构化提取结果转为 Mermaid 拓扑描述
"""


def build_teaser_topology(extraction_result: str, style: str = "academic") -> str:
    """
    生成 teaser（概念图/动机图）的 Mermaid 拓扑
    """
    prompt = f"""你是一个专业的学术图表工程师。根据以下提取的结构化信息，生成一个 Mermaid flowchart 描述。

## 要求
- 输出必须是合法的 Mermaid flowchart LR 或 TB 语法
- 使用简洁的英文节点描述（每个节点文字不超过 10 个词）
- 包含节点之间的箭头连接
- 用 subgraph 分组相关概念
- 不要有任何中文字符

## 风格
- academic: 干净简洁，适合顶会论文
- 先展示痛点（旧方法），再展示本文解决方案（新方法）
- 使用对比色区分旧方法（红色调）和新方法（蓝色调）

## 输出格式
只输出 Mermaid 代码，不要任何解释，格式如下：
```mermaid
flowchart LR
    style old_method fill:#ffcccc
    style new_method fill:#cce5ff
    ...
```

## 提取的信息
{extraction_result}
"""
    return prompt


def build_architecture_topology(extraction_result: str, style: str = "academic") -> str:
    """
    生成 architecture（主框架图）的 Mermaid 拓扑
    这是最核心的模块！
    """
    prompt = f"""你是一个专业的学术图表工程师。根据以下提取的结构化信息，生成一个精确的 Mermaid flowchart LR（从左到右）拓扑描述。

## 严格规则
1. **输出必须是合法的 Mermaid 代码**，直接可以被渲染
2. **所有节点 ID 必须是唯一的英文标识符**（如 enc1, att1, dec1）
3. **所有节点 label 使用方括号** `[label text]`
4. **箭头格式**：`A-->B` 或 `A-->|label|B`
5. **绝对不能有任何中文字符**
6. **节点内文字要简洁**，每个节点不超过 8 个英文单词
7. **用 subgraph 分层**：Input/Encoder/Decoder/Output
8. **数据流向必须清晰**：严格从左到右

## 节点样式建议（通过 Mermaid 语法）
```
classDef encoder fill:#E3F2FD,stroke:#1565C0,stroke-width:2px
classDef decoder fill:#E8F5E9,stroke:#2E7D32,stroke-width:2px
classDef attention fill:#FFF8E1,stroke:#F9A825,stroke-width:2px
classDef io fill:#F3E5F5,stroke:#7B1FA2,stroke-width:2px
classDef fc fill:#FBE9E7,stroke:#BF360C,stroke-width:2px
class A classDef
```

## 提取的结构化信息
{extraction_result}

请直接输出 Mermaid 代码：
"""
    return prompt


def build_flowchart_topology(extraction_result: str, style: str = "academic") -> str:
    """
    生成 flowchart（算法流程图）的 Mermaid 拓扑
    """
    prompt = f"""你是一个专业的学术图表工程师。根据以下提取的算法流程信息，生成一个 Mermaid flowchart TD（从上到下）拓扑描述。

## 严格规则
1. **输出必须是合法的 Mermaid flowchart TD 代码**
2. **使用圆角矩形表示普通步骤**：`[Step description]`
3. **使用菱形表示条件分支**：`{{condition?}}`
4. **使用椭圆形表示开始/结束**：`([Start])` / `([End])`
5. **循环用 subgraph 包裹并加粗边框**
6. **绝对不能有任何中文字符**
7. **每步描述不超过 10 个英文单词**

## 提取的算法步骤
{extraction_result}

请直接输出 Mermaid 代码：
"""
    return prompt


def build_environment_topology(extraction_result: str, style: str = "academic") -> str:
    """
    生成 environment（实验环境图）的 Mermaid 拓扑
    """
    prompt = f"""你是一个专业的学术图表工程师。根据以下提取的实验环境信息，生成一个 Mermaid flowchart LR（从左到右）拓扑描述。

## 严格规则
1. **输出必须是合法的 Mermaid 代码**
2. **展示 Agent-Environment 交互闭环**
3. **用不同颜色区分 Agent 和 Environment**
4. **绝对不能有任何中文字符**
5. **节点标签简洁，每个不超过 8 个英文单词**

## 风格
- Agent 节点用蓝色调
- Environment 节点用绿色调
- 交互箭头加 label 表示交互类型（state/action/reward）

## 提取的环境信息
{extraction_result}

请直接输出 Mermaid 代码：
"""
    return prompt


def convert_to_topology(figure_type: str, extraction_result: str, style: str = "academic") -> str:
    """
    主入口：根据图类型分发到对应的拓扑生成器
    """
    if figure_type == "teaser":
        return build_teaser_topology(extraction_result, style)
    elif figure_type == "architecture":
        return build_architecture_topology(extraction_result, style)
    elif figure_type == "flowchart":
        return build_flowchart_topology(extraction_result, style)
    elif figure_type == "environment":
        return build_environment_topology(extraction_result, style)
    else:
        # 默认用架构图
        return build_architecture_topology(extraction_result, style)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)