Skill: yang1bai/nature-paper-hub (version 1.0.0)

--- name: nature-paper-hub description: Full-pipeline Nature-series journal writing assistant. Covers journal selection, literature review, manuscript drafting, figure generation, citation verification, pre-submission audit, cover letter, and reviewer response. Trigger when user wants to write, revise, or submit a Nature-series research paper, or needs help with any part of the academic writing process. version: 1.0.0 author: Yang1Bai tags: - academic-writing - nature-journal - scientific-writing - research-paper - latex - claude-code - codex - openclaw --- # Nature Paper Hub ## Description Full-pipeline Nature-series journal writing assistant. Trigger when the user wants to: - Write, draft, or outline a Nature-series research paper - Select a Nature journal for submission - Revise any section of a manuscript - Plan or improve figures - Check citations or generate reference lists - Prepare a submission checklist or rebuttal letter - Export manuscript as LaTeX (Overleaf) or Word Multi-language: interact in Chinese or English; all manuscript output is in English. ## Skill Location ~/.openclaw/workspace/skills/nature-paper-hub/ ## Supporting Files - templates/journal-specs.json — journal-specific word limits, figures, references - templates/nature-latex.tex — master LaTeX template (Overleaf-ready) - scripts/export_docx.py — Word export via python-docx --- ## STAGE 0 — Journal Selection **Always run this stage first unless the user has already specified a journal.** Present this menu and ask the user to choose: ``` 📋 请选择目标期刊 / Select target journal: 1. Nature (IF 63.7) — 顶级综合科学 2. Nature Materials (IF 37.2) — 材料科学 3. Nature Chemistry (IF 19.2) — 化学 4. Nature Energy (IF 60.9) — 能源 5. Nature Catalysis (IF 37.8) — 催化 6. Nature Sustainability (IF 25.1) — 可持续发展 7. Nature Communications (IF 15.7) — 全科学，开放获取，最灵活 8. Nature Methods (IF 32.1) — 方法学 9. Nature Computational Science (IF 12.0) — 计算科学 10. Nature Chemical Engineering (IF 13.0) — 化学工程 11. Nature Machine Intelligence (IF 23.9) — 机器学习/AI/机器人 12. Nature Synthesis (IF 20.0) — 合成化学与材料合成 13. 其他 / Other — 请告诉我期刊名 ``` After selection, load the corresponding entry from `templates/journal-specs.json` and display: - Word limits (body, abstract, Methods) - Figure/table limit - Reference limit - Methods location (within text vs. after references) - Acceptance rate and IF Then ask: **"您的论文类型是 Article 还是 Letter？"** ### ⚠️ Journal-specific special rules to load: **Nature Synthesis (选12):** - NO schemes — all graphics must be figures (no reaction scheme format) - Methods section CANNOT contain figures or tables — use Extended Data or SI - Results and Discussion may be combined into one section with subheadings - Discussion must be succinct and cannot have subheadings - Only one article type: Article (covers both short comms and full papers) **Nature Machine Intelligence (选11):** - Also accepts Analysis type (100–150 word abstract) - Reviews: ≤10% of references should have short annotations explaining key contributions - Strong preference for reproducibility: code/data availability is heavily weighted **Nature Chemical Engineering (选10):** - Methods called "Online Methods", placed after Discussion - Extended Data: up to 10 figures allowed - Focus on chemical engineering relevance: must address scale-up, process, or engineering challenge --- ## STAGE 1 — Concept & Literature Review ### 1a. Concept Definition Ask the user: 1. **Research topic in one sentence** (用一句话描述研究内容) 2. **Core innovation** — what makes this new? (核心创新点是什么？) 3. **Key result** — what did you find/achieve? (最重要的结果/发现) 4. **Target scope** — does it fit the selected journal's scope? ### 1b. Literature Search Use the LitReview system at https://ybliterature.com/api/search?q=<query> to search for related papers. Also use web_search with queries like: - `site:nature.com "<topic>" filetype:pdf` - `arxiv.org "<topic>" Nature-style` Search for 3–5 open-access papers from the target journal as structural templates: ``` Search query: site:nature.com/[journal-shortname] "<topic keyword>" open access ``` For each found paper, extract: - Paper structure (section titles used) - Abstract style - Figure count and types Present the user with: key gap in literature, positioning suggestion, and 3–5 recommended template papers. ### 1c. Novelty Check Ask: "Has anyone published very similar work in the past 2 years?" Run a targeted web search. Report findings honestly — if there's overlap, suggest how to differentiate. --- ## STAGE 2 — Outline & Structure Planning Based on the journal selected and paper type, generate a tailored outline. ### Standard Nature Article Outline: ``` Title: [concise, ≤15 words, no abbreviations] Abstract: [150 words — context → problem → approach → key result → significance] Introduction ¶1 Broad context and importance ¶2 Specific background — what is known ¶3 The gap or unsolved problem ¶4 Your approach and key findings (end: "Here we report...") Results Section 1: [Synthesis/Preparation/Model — first evidence] Section 2: [Characterization/Validation — structural/spectroscopic proof] Section 3: [Mechanism/Explanation — why it works] Section 4: [Performance/Application — how good it is] Section 5: [Generalizability/Comparison — how broad/better] Discussion ¶1 Summary of key findings ¶2 Comparison with literature ¶3 Mechanistic interpretation ¶4 Limitations + future work ¶5 Broader impact (1 sentence) Methods [~3000 words, after refs for most journals] - Materials/Reagents - Synthesis/Preparation - Characterization techniques - Computational details (if applicable) - Statistical analysis References [numbered, order of appearance] Figure Legends [detailed, self-contained] Extended Data [optional, up to 10 items] ``` **Adjust the outline based on paper type and journal.** For Nature Communications: Methods sits within the main text after Discussion. Ask the user to review and modify the outline before proceeding. --- ## STAGE 3 — Section-by-Section Writing Work through each section one at a time. Ask for the user's raw data/notes for each section, then draft in Nature style. ### Abstract Writing Rules (Nature Portfolio): - Single paragraph, no citations, no undefined abbreviations - Sentence 1–2: Broad context (why does this matter globally?) - Sentence 3–4: Specific problem or gap - Sentence 5–6: Your approach/method (brief) - Sentence 7–8: Key quantitative results - Sentence 9–10: Significance and outlook - Target: exactly 150 words (or journal limit) - Tense: Present for known facts; Past for what you did; Present for conclusions **📊 After drafting abstract — always run word count check:** ``` Current word count: [X] / [journal limit] Status: [✅ within limit | ⚠️ X words over — suggest cuts below] ``` If over limit, suggest specific cuts: remove adjectives, merge sentences, cut background context. ### Introduction Writing Rules: - 4–6 paragraphs, ~800 words total - Each paragraph has a clear topic sentence - Citations must be accurate — verify with web_search if uncertain - Final paragraph: explicitly state what this paper reports - Avoid: "In this paper, we..." (use "Here we show/report/demonstrate...") - Avoid: excessive self-citation ### Results Writing Rules: - Lead each subsection with the key finding (topic sentence = result) - Present data before interpretation - Every figure/table must be cited in order (Fig. 1a, Fig. 1b, Fig. 2...) - Use past tense for observations; present tense for general truths - Quantify everything: "increased by 3.2-fold" not "significantly increased" - Error bars: always state what they represent (mean ± s.d., n = X) ### Discussion Writing Rules: - Do NOT restate Results — interpret and contextualize them - Compare explicitly with the best prior work (with citations) - Address limitations honestly (reviewers will ask if you don't) - End with 1 sentence of broader impact ### Methods Writing Rules: - Enough detail for independent reproduction - Include all instrument models, software versions, parameters - For computational work: functional, basis set, k-points, cutoff energy, software version - Statistical methods: which test, software, significance threshold (p < 0.05) - Ethics/IRB statements if applicable ### 🔍 Post-Section Self-Critique (run after drafting EVERY section) After delivering each drafted section, immediately evaluate it from a Nature reviewer's perspective: ``` 📋 Self-critique — [Section Name]: ✅ Strengths: - [what works well] ⚠️ Weaknesses / likely reviewer concerns: - [specific issue 1: e.g., "Claim in ¶2 lacks quantitative support"] - [specific issue 2: e.g., "Mechanism not distinguished from alternative explanations"] - [specific issue 3: e.g., "'Significantly' used without p-value"] 💡 Suggested improvements: - [concrete fix for each weakness] ``` Do NOT skip this step. If the user wants to proceed anyway, acknowledge the risks. --- ## STAGE 4 — Figure Planning Ask user: how many figures do you have data for? (Must be ≤ journal limit) For each figure, guide: ``` Figure X: [What story does this figure tell?] Panel (a): [Data type] — [Message] Panel (b): [Data type] — [Message] Panel (c): [Data type] — [Message] Design rules: - Each figure tells ONE clear story - Panel a = overview/schematic; subsequent panels = evidence - Resolution: 300 DPI min (600 DPI for line art) - Font: Arial or Helvetica, ≥7pt in final printed size - Color: accessible palette (avoid red-green for colorblind readers) - Scale bars: always include for microscopy images - Statistical indicators: *, **, *** for significance; exact p-values preferred ``` Suggest figure order: schematic → characterization → mechanism → performance → application --- ## STAGE 5 — Citation Verification For each reference cited in the manuscript: 1. Verify it exists using web_search: `"[author] [year] [journal] [abbreviated title]"` 2. Check if it's been retracted: search `"[paper title] retraction"` 3. Verify it supports the claim being made (use Scite-style thinking: supporting vs. contrasting) 4. Format in Nature numbered style: ``` 1. LastName, A., LastName, B. & LastName, C. Title of paper. Journal Vol, pages (Year). ``` Flag any: - References older than 10 years (unless seminal) - References that don't directly support the claim - Missing DOIs ### 📋 Bulk Reference Formatting (quick mode) If user pastes a list of references in any format (Google Scholar export, DOI list, messy copy-paste): 1. Parse each entry — extract authors, year, title, journal, volume, pages, DOI 2. For any missing fields, look up via CrossRef: `web_fetch("https://api.crossref.org/works/<DOI>")` 3. Re-format ALL entries into Nature numbered style in one batch 4. Also output a `.bib` BibTeX block for the entire list 5. Flag any entries that could not be verified Trigger phrase: "帮我格式化引用" / "format my references" / "整理参考文献" --- ## STAGE 6 — Pre-Submission Audit Run through this checklist before export: ### Formatting ✓ - [ ] Word count within journal limit (body text only, excl. abstract/refs/legends) - [ ] Abstract within word limit, single paragraph, no citations - [ ] Figure count ≤ journal limit - [ ] Reference count ≤ journal limit - [ ] Methods location correct for journal - [ ] Line numbers enabled (for peer review) ### Content ✓ - [ ] Title ≤ 15 words, no abbreviations - [ ] All abbreviations defined at first use - [ ] All figures cited in order in text - [ ] All figure legends self-contained (scale bars, error bars, n values, stats) - [ ] Data availability statement present - [ ] Author contributions (CRediT taxonomy) - [ ] Competing interests declared - [ ] Acknowledgements include all funding with grant numbers ### Science ✓ - [ ] Claims match data (no overclaiming) - [ ] Statistics correct (appropriate test, reported correctly) - [ ] Controls included and described - [ ] Reproducibility: n ≥ 3 for key experiments ### Journal-Specific ✓ - [ ] Cover letter written (highlight novelty + fit for journal) - [ ] Suggested reviewers (3–5 names + emails + no conflict) - [ ] Excluded reviewers (if any) ### 📝 Cover Letter Generation After checklist is complete, automatically generate a cover letter: ``` [Date] Dear [Editor-in-Chief / Editors of {Journal}], We are pleased to submit our manuscript entitled "[Title]" for consideration as a [Article/Letter] in [Journal]. [Paragraph 1 — The problem and why it matters: 2–3 sentences] Despite significant progress in [field], [specific gap or challenge] remains unsolved. Addressing this challenge is critical because [broader impact]. [Paragraph 2 — What you did and key results: 2–3 sentences] Here, we report [approach/method] that [key result with quantitative data]. Notably, [most impressive finding, e.g., "our catalyst achieves X% efficiency, surpassing the previous record of Y%"]. [Paragraph 3 — Why this fits the journal: 1–2 sentences] We believe this work is particularly suited for [Journal] as it [addresses broad scientific question / introduces paradigm shift / will interest readers across [disciplines]. This manuscript has not been published elsewhere and is not under consideration by any other journal. All authors have approved the submission. We suggest the following reviewers: [Name, Affiliation, email] ... Thank you for your consideration. Sincerely, [Corresponding Author] [Affiliation, email] ``` Adjust tone based on journal prestige: Nature/Nature Materials → more assertive; Nature Communications → slightly more measured. --- ## STAGE 7 — Export Ask user: **"导出格式？Overleaf (LaTeX) 还是 Word (.docx)？"** ### Option A: LaTeX / Overleaf 1. Load template from `templates/nature-latex.tex` 2. Fill in all sections with the drafted content 3. Generate `main.tex` and `references.bib` (BibTeX format) 4. Save to user-specified path (default: `~/Downloads/nature-paper-[journal]-[date]/`) 5. Instructions: "Upload main.tex + references.bib + figure files to Overleaf as a new project" ### Option B: Word (.docx) 1. Run `python3 ~/.openclaw/workspace/skills/nature-paper-hub/scripts/export_docx.py` 2. Script takes the drafted sections and generates a properly formatted .docx 3. Styles: Heading 1 for sections, 11pt Times New Roman body, double-spaced 4. Save to `~/Downloads/nature-paper-[journal]-[date].docx` --- ## STAGE 8 — Rebuttal Response When the user receives reviewer comments: ### Step 1: Triage — classify ALL comments before writing any response First, parse and classify every comment: ``` 📊 Reviewer Comment Triage: Reviewer 1: Comment 1: [summary] → 🔴 Major | Needs new experiment Comment 2: [summary] → 🟡 Major | Needs clarification/additional analysis Comment 3: [summary] → 🟢 Minor | Text revision only Comment 4: [summary] → ✅ Valid concern | ❌ Disagree — evidence-based Reviewer 2: ... 📋 Revision Strategy: New experiments needed: [list] New analyses needed: [list] Text-only revisions: [list] Planned disagreements: [list with justification] Estimated revision effort: [X weeks] ``` Present this triage to the user and confirm strategy before writing responses. ### Step 2: Write point-by-point responses For each comment (after triage confirmed): ``` **Reviewer X, Comment Y:** [🔴/🟡/🟢] [Quote the comment exactly] **Response:** We thank the reviewer for this [insightful/constructive] comment. [Acknowledge validity of concern.] [Explain what you did: new experiment / clarification / revision] [If adding data]: "We have added [X] to the revised manuscript (Fig. X / Line X)." [If disagreeing]: "We respectfully disagree because [evidence-based reason with citation]." **Manuscript change:** [Quote revised text with line numbers, or state "no change required"] ``` ### Step 3: Generate revision cover letter After all responses: - Summary of major changes (numbered) - List of new figures/data added - Statement of how each reviewer's concerns were addressed - Tone: confident but respectful --- ## Integration: Literature Search (two-tier) ### Tier 1 — Static papers index (available to ALL users) The repo includes `data/papers-index.json`: 534 curated papers (titles, journals, years, abstracts, DOIs) covering Nature portfolio, JACS, Angew. Chem., Adv. Mater., npj Computational Materials, and more. Load and search it locally: ```python import json with open('~/.openclaw/workspace/skills/nature-paper-hub/data/papers-index.json') as f: index = json.load(f)['papers'] # Simple keyword match: results = [p for p in index if query.lower() in (p['title']+p['abstract']).lower()] ``` Use for: finding relevant papers to cite, checking what's published, writing style reference. ### Tier 2 — Personal LitReview system (owner only) API: GET https://ybliterature.com/api/search?q=<query> (requires authentication — owner use only) Always query Tier 1 first; use Tier 2 only when owner is running the session. Example call: `web_fetch("https://ybliterature.com/api/search?q=electrocatalysis+oxygen+evolution")` ### RAG-enhanced writing (use when drafting any section): Before drafting Introduction, Results, or Discussion: 1. Query LitReview for the paper topic: ``` web_fetch("https://ybliterature.com/api/search?q=<topic>") ``` 2. **Filter returned results — only use high-impact journal papers as style anchors:** Priority tier (use for style): Nature, Nature [sub-journals], Science, Cell, JACS, Angew. Chem., Adv. Mater., ACS Nano Skip for style (still valid as citations): Electrochimica Acta, JES, Surf. Coat. Technol., J. Alloys Compd., and other engineering/applied journals If the returned results are mostly lower-tier journals, supplement with: ``` web_search("site:nature.com <topic> <year>") ``` 3. From the **filtered** papers, note: - How they open the Introduction (first sentence patterns) - How Results subsections are titled (use action phrases, not nouns) - How Discussion compares with prior work - Sentence structures used to present quantitative data 4. Use these as **style anchors** when drafting — mirror the register, hedging language, and argumentation patterns of real Nature papers in the same field (not generic academic writing). 5. When quoting style patterns, attribute: "[modelled on: Author et al., Journal, Year]" ### CrossRef metadata enrichment: For any paper found in LitReview or cited by the user: ``` web_fetch("https://api.crossref.org/works/<DOI>") ``` This returns: full author list, exact title, volume/pages, citation count, funder info. Use citation count as a proxy for impact when recommending references. --- ## Language & Interaction - Interact with user in Chinese (or whichever language they use) - All manuscript drafts, templates, and exports are in English - When asking for input about experiments/data, accept Chinese descriptions and translate to academic English - When uncertain about a translation of a scientific term, provide both Chinese and English and ask for confirmation --- ## Quick Commands The user can say any of these to jump to a specific stage: - "选刊" / "choose journal" → Stage 0 - "文献综述" / "literature review" → Stage 1 - "写大纲" / "outline" → Stage 2 - "写[某章节]" / "write [section]" → Stage 3 - "图表规划" / "figure plan" → Stage 4 - "检查引用" / "check citations" → Stage 5 - "格式化引用" / "format references" → Stage 5 bulk mode - "投稿检查" / "submission check" → Stage 6 - "写cover letter" / "cover letter" → Stage 6 cover letter - "导出" / "export" → Stage 7 - "写回复信" / "rebuttal" → Stage 8 - "审稿意见分类" / "triage reviewers" → Stage 8 triage only - "从头开始" / "start new paper" → Stage 0

path: README.md
content: # 🧬 Nature Paper Hub

<p align="center">
  <img src="https://img.shields.io/badge/Nature_Journals-9_supported-blue?style=flat-square" />
  <img src="https://img.shields.io/badge/OpenClaw-compatible-green?style=flat-square" />
  <img src="https://img.shields.io/badge/Claude_Code-plugin-purple?style=flat-square" />
  <img src="https://img.shields.io/badge/Codex-compatible-orange?style=flat-square" />
  <img src="https://img.shields.io/badge/License-MIT-lightgrey?style=flat-square" />
</p>

<p align="center">
  <a href="#chinese">中文版</a> · <a href="#english">English</a>
</p>

---

<h2 id="chinese">🇨🇳 中文版</h2>

### 简介

`nature-paper-hub` 是一套面向 Nature 系列期刊投稿的全流程 AI 写作 agent，覆盖从**选刊、文献调研、论文起草、图表生成、引用核验、投稿前检查，到审稿意见回复**的完整链路。

支持 **OpenClaw、Claude Code、Codex** 三种平台，输出格式支持 **LaTeX（Overleaf）和 Word（.docx）**，并内置 CSV/Excel 自动生图、CrossRef 实时引用核验、个人文献库 RAG 写作风格锚定。

---

### 功能一览

| Skill | 功能说明 |
|-------|----------|
| 🏠 `nature-paper-hub` | 主入口，8 阶段全流程对话路由 |
| 📊 `nature-figure` | CSV/Excel → Nature 风格 matplotlib/R 图，自动识别图表类型 |
| 📖 `nature-reader` | PDF/DOI/arXiv → 双语精读 Markdown，含图表解读与引用建议 |
| 📚 `nature-citation` | CrossRef 实时核验 + RetractionWatch 撤稿检查 + BibTeX/RIS/ENW/Zotero 导出 |
| 🎞️ `nature-paper2ppt` | 论文 → 中文/双语 PPTX，含演讲者备注 |

---

### 支持的 Nature 子刊

| 期刊 | 影响因子 | 接收率 | 正文字数 | 图数 | 引用 |
|------|---------|--------|---------|------|------|
| Nature | 63.7 | ~8% | 3,000 | 6 | 30 |
| Nature Materials | 37.2 | ~9% | 3,000 | 6 | 50 |
| Nature Chemistry | 19.2 | ~9% | 3,000 | 6 | 50 |
| Nature Energy | 60.9 | ~8% | 3,000 | 6 | 50 |
| Nature Catalysis | 37.8 | ~8% | 3,000 | 6 | 50 |
| Nature Sustainability | 25.1 | ~10% | 4,000 | 8 | 60 |
| Nature Communications | 15.7 | ~20% | 5,000† | 10 | 60 |
| Nature Methods | 32.1 | ~8-10% | 3,000 | 6 | 50 |
| Nature Computational Science | 12.0 | ~12% | 5,000 | 8 | 60 |
| **Nature Chemical Engineering** | **13.0** | **~8%** | **3,500** | **6+10ED** | **50** |
| **Nature Machine Intelligence** | **23.9** | **~8%** | **3,500** | **6** | **50** |
| **Nature Synthesis** | **20.0** | **~8%** | **3,000** | **6‡** | **50** |

† Nature Communications 字数含 Methods 章节  
‡ Nature Synthesis 不接受 Scheme，只接受 Figure；Methods 中不可含图表

---

### 安装

#### 方式一：OpenClaw

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git
cp -R nature-paper-hub ~/.openclaw/workspace/skills/
# 重启 OpenClaw，对话中说"选刊"即可激活
```

#### 方式二：Claude Code

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git

# 安装为用户级 subagents（全局可用）
mkdir -p ~/.claude/agents
cp nature-paper-hub/skills/nature-figure/SKILL.md ~/.claude/agents/nature-figure.md
cp nature-paper-hub/skills/nature-reader/SKILL.md ~/.claude/agents/nature-reader.md
cp nature-paper-hub/skills/nature-citation/SKILL.md ~/.claude/agents/nature-citation.md
cp nature-paper-hub/skills/nature-paper2ppt/SKILL.md ~/.claude/agents/nature-paper2ppt.md
cp nature-paper-hub/SKILL.md ~/.claude/agents/nature-paper-hub.md
```

或安装为项目级 subagents（仅当前项目可用）：
```bash
mkdir -p .claude/agents
cp nature-paper-hub/skills/*/SKILL.md .claude/agents/
```

安装完成后，在 Claude Code 中直接对话触发即可（无需额外命令）：
```
Use nature-figure to generate a matplotlib figure from my data.csv
```

#### 方式三：Codex

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git

# 创建 skills 目录（默认不存在）并安装
mkdir -p ~/.codex/skills
for d in nature-paper-hub/skills/nature-*; do
  cp -R "$d" ~/.codex/skills/
done

# 验证安装
ls ~/.codex/skills/
# 重启 Codex 后生效
```

#### 安装 Python 依赖

```bash
pip install -r nature-paper-hub/scripts/requirements.txt
```

---

### 快速上手

#### OpenClaw / Telegram
在对话中输入以下关键词直接跳转对应阶段：

```
选刊         → 选择目标 Nature 子刊，显示字数/图数/引用限制
文献综述     → 调用个人文献库 + 网络搜索
写大纲       → 按子刊生成定制化论文结构
写摘要       → 起草符合 Nature 风格的 Abstract（150词）
图表规划     → 规划每张图的叙事逻辑
检查引用     → CrossRef 实时核验 + 撤稿检查
导出         → 选择 LaTeX（Overleaf）或 Word
写回复信     → 逐条审稿意见生成回复框架
```

#### Claude Code
```
Use nature-figure to generate a matplotlib figure from my data.csv
Use nature-reader to create a bilingual reader for this paper: ~/Downloads/paper.pdf
Use nature-citation to verify my reference list and export as BibTeX
Use nature-paper2ppt to convert this paper into a Chinese presentation
```

#### Codex
```
Use the nature-figure skill to plot my experimental results from data.xlsx
Use nature-reader to translate this Nature paper and ground each figure
Use nature-citation to find supporting references for oxygen evolution catalysis
```

---

### 全流程说明

```
阶段 0: 选刊           → 选目标子刊，加载对应字数/图数/引用限制
阶段 1: 文献调研       → LitReview 个人库 + CrossRef + 网络搜索
阶段 2: 结构规划       → 按期刊类型定制论文大纲
阶段 3: 逐节起草       → Abstract / Introduction / Results / Discussion / Methods
阶段 4: 图表生成       → CSV/Excel 自动出图 + matplotlib/R 代码 + 图例文字
阶段 5: 引用核验       → CrossRef API + RetractionWatch + 多格式导出
阶段 6: 投稿前检查     → 字数/图数/格式/内容完整 checklist
阶段 7: 导出           → LaTeX (Overleaf) / Word (.docx) / PPTX
阶段 8: 审稿回复       → 逐条回复框架 + 修改说明 + 封面信
```

---

### 文件结构

```
nature-paper-hub/
├── README.md                    # 本文件（中英双语）
├── SKILL.md                     # 主 skill（OpenClaw 入口）
├── skills/
│   ├── nature-figure/SKILL.md   # 科研绘图（自动识别 + matplotlib/R）
│   ├── nature-reader/SKILL.md   # 双语论文精读器
│   ├── nature-citation/SKILL.md # CrossRef 引用核验 + 多格式导出
│   └── nature-paper2ppt/SKILL.md# 论文转 PPT
├── templates/
│   ├── journal-specs.json       # 9个子刊规格数据
│   └── nature-latex.tex         # Overleaf LaTeX 模板
└── scripts/
    ├── auto_figure.py           # CSV/Excel → Nature 图（540行）
    ├── export_docx.py           # Word 导出
    ├── export_pptx.py           # PPT 导出
    └── requirements.txt         # Python 依赖
```

---

### 与同类项目对比

| 功能 | **nature-paper-hub** | Yuan1z0825/nature-skills | Boom5426/Nature-Paper-Skills |
|------|:---:|:---:|:---:|
| 9个子刊精确规格（字数/图/引用） | ✅ | ❌ | ✅ |
| 全流程单入口路由 | ✅ | ❌ | ✅ |
| LaTeX / Overleaf 模板 | ✅ | ❌ | ❌ |
| Word 导出（.docx） | ✅ | ❌ | ❌ |
| matplotlib/R 科研绘图代码 | ✅ | ✅ | ❌ |
| CSV/Excel → 自动生图 | ✅ | ❌ | ❌ |
| 双语论文精读器 | ✅ | ✅ | ❌ |
| 论文转 PPT（PPTX） | ✅ | ✅ | ❌ |
| 引用多格式导出（BibTeX/RIS/ENW/Zotero） | ✅ | ✅ | ❌ |
| CrossRef API 实时引用核验 | ✅ | ❌ | ❌ |
| RetractionWatch 撤稿检查 | ✅ | ❌ | ❌ |
| LitReview RAG 写作风格锚定 | ✅ | ❌ | ❌ |
| 个人文献库集成 | ✅ | ❌ | ❌ |
| Claude Code 插件 | ✅ | ✅ | ✅ |
| Codex 兼容 | ✅ | ✅ | ✅ |
| OpenClaw 兼容 | ✅ | ❌ | ❌ |

---

### 致谢

设计灵感部分来源于：
- [Yuan1z0825/nature-skills](https://github.com/Yuan1z0825/nature-skills)（上海交通大学袁一哲团队）
- [Boom5426/Nature-Paper-Skills](https://github.com/Boom5426/Nature-Paper-Skills)
- [Nature Portfolio Author Guidelines](https://www.nature.com/authors)

---

### 许可证

MIT License — 自由使用、修改和分发，保留原始署名即可。

---

### 贡献指南

欢迎提 Issue 和 PR！请说明：
1. 目标期刊
2. 功能缺口或 bug 描述
3. 建议实现方式

---
---

<h2 id="english">🇬🇧 English</h2>

### Introduction

`nature-paper-hub` is a full-pipeline AI writing agent for Nature-series journal submissions. It covers every stage from **journal selection, literature review, manuscript drafting, figure generation, citation verification, pre-submission audit, to reviewer response**.

Compatible with **OpenClaw, Claude Code, and Codex**. Outputs **LaTeX (Overleaf-ready) and Word (.docx)**. Features include automatic figure generation from CSV/Excel, CrossRef real-time citation verification, and RAG-enhanced writing style grounded in your personal literature library.

---

### Features

| Skill | Description |
|-------|-------------|
| 🏠 `nature-paper-hub` | Main hub with 8-stage conversational pipeline routing |
| 📊 `nature-figure` | CSV/Excel → Nature-style matplotlib/R figures with auto chart-type detection |
| 📖 `nature-reader` | PDF/DOI/arXiv → bilingual annotated Markdown with figure grounding and citation suggestions |
| 📚 `nature-citation` | CrossRef real-time verification + RetractionWatch check + BibTeX/RIS/ENW/Zotero export |
| 🎞️ `nature-paper2ppt` | Paper → Chinese/bilingual PPTX with presenter notes |

---

### Supported Journals

| Journal | IF | Accept | Words | Figs | Refs |
|---------|:---:|:---:|:---:|:---:|:---:|
| Nature | 63.7 | ~8% | 3,000 | 6 | 30 |
| Nature Materials | 37.2 | ~9% | 3,000 | 6 | 50 |
| Nature Chemistry | 19.2 | ~9% | 3,000 | 6 | 50 |
| Nature Energy | 60.9 | ~8% | 3,000 | 6 | 50 |
| Nature Catalysis | 37.8 | ~8% | 3,000 | 6 | 50 |
| Nature Sustainability | 25.1 | ~10% | 4,000 | 8 | 60 |
| Nature Communications | 15.7 | ~20% | 5,000† | 10 | 60 |
| Nature Methods | 32.1 | ~8-10% | 3,000 | 6 | 50 |
| Nature Computational Science | 12.0 | ~12% | 5,000 | 8 | 60 |
| **Nature Chemical Engineering** | **13.0** | **~8%** | **3,500** | **6+10ED** | **50** |
| **Nature Machine Intelligence** | **23.9** | **~8%** | **3,500** | **6** | **50** |
| **Nature Synthesis** | **20.0** | **~8%** | **3,000** | **6‡** | **50** |

† Nature Communications word count includes Methods section  
‡ Nature Synthesis does not accept Schemes — figures only; Methods section cannot contain figures or tables

---

### Installation

#### Option 1: OpenClaw

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git
cp -R nature-paper-hub ~/.openclaw/workspace/skills/
# Restart OpenClaw. Say "选刊" or "choose journal" to activate.
```

#### Option 2: Claude Code

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git

# Install as user-level subagents (available in all projects)
mkdir -p ~/.claude/agents
cp nature-paper-hub/skills/nature-figure/SKILL.md ~/.claude/agents/nature-figure.md
cp nature-paper-hub/skills/nature-reader/SKILL.md ~/.claude/agents/nature-reader.md
cp nature-paper-hub/skills/nature-citation/SKILL.md ~/.claude/agents/nature-citation.md
cp nature-paper-hub/skills/nature-paper2ppt/SKILL.md ~/.claude/agents/nature-paper2ppt.md
cp nature-paper-hub/SKILL.md ~/.claude/agents/nature-paper-hub.md
```

Or install as project-level subagents (current project only):
```bash
mkdir -p .claude/agents
cp nature-paper-hub/skills/*/SKILL.md .claude/agents/
```

Once installed, trigger naturally in conversation — no extra commands needed:
```
Use nature-figure to generate a matplotlib figure from my data.csv
```

#### Option 3: Codex

```bash
git clone https://github.com/Yang1Bai/nature-paper-hub.git

# Create skills directory (does not exist by default) and install
mkdir -p ~/.codex/skills
for d in nature-paper-hub/skills/nature-*; do
  cp -R "$d" ~/.codex/skills/
done

# Verify installation
ls ~/.codex/skills/
# Restart Codex to pick up new skills.
```

#### Install Python dependencies

```bash
pip install -r nature-paper-hub/scripts/requirements.txt
```

---

### Quick Start

#### OpenClaw / Telegram
Say any of these in conversation to jump to that stage:

```
选刊 / choose journal    → Select target journal; see word/figure/ref limits
文献综述 / literature    → Search personal library + CrossRef + web
写大纲 / outline         → Generate journal-specific manuscript structure
写摘要 / abstract        → Draft Nature-style Abstract (150 words)
图表规划 / figure plan   → Plan figure narrative logic
检查引用 / citations     → CrossRef verify + retraction check
导出 / export            → LaTeX (Overleaf) or Word
写回复信 / rebuttal      → Point-by-point reviewer response framework
```

#### Claude Code
```
Use nature-figure to generate a matplotlib figure from my data.csv
Use nature-reader to create a bilingual reader for this paper: ~/Downloads/paper.pdf
Use nature-citation to verify my reference list and export as BibTeX
Use nature-paper2ppt to convert this paper into a Chinese presentation
```

#### Codex
```
Use the nature-figure skill to plot my experimental results from data.xlsx
Use nature-reader to translate this Nature paper and ground each figure
Use nature-citation to find supporting references for oxygen evolution catalysis
```

---

### Full Pipeline

```
Stage 0: Journal Selection    → Choose sub-journal; load word/figure/ref limits
Stage 1: Literature Review    → Personal library + CrossRef + web search
Stage 2: Outline Planning     → Journal-tailored manuscript structure
Stage 3: Section Drafting     → Abstract / Intro / Results / Discussion / Methods
Stage 4: Figure Generation    → Auto-plot from CSV/Excel + matplotlib/R code + legend text
Stage 5: Citation Check       → CrossRef API + RetractionWatch + multi-format export
Stage 6: Pre-submission Audit → Word count / figure count / format / content checklist
Stage 7: Export               → LaTeX (Overleaf) / Word (.docx) / PPTX
Stage 8: Reviewer Response    → Point-by-point reply + change summary + cover letter
```

---

### Repository Structure

```
nature-paper-hub/
├── README.md                    # This file (bilingual CN/EN)
├── SKILL.md                     # Main skill (OpenClaw entry point)
├── skills/
│   ├── nature-figure/SKILL.md   # Figure generation (auto-detect + matplotlib/R)
│   ├── nature-reader/SKILL.md   # Bilingual paper reader
│   ├── nature-citation/SKILL.md # CrossRef citation verification + multi-format export
│   └── nature-paper2ppt/SKILL.md# Paper to presentation
├── templates/
│   ├── journal-specs.json       # Per-journal specs (9 journals)
│   └── nature-latex.tex         # Overleaf-ready LaTeX template
└── scripts/
    ├── auto_figure.py           # CSV/Excel → Nature figure (540 lines)
    ├── export_docx.py           # Word export
    ├── export_pptx.py           # PPTX export
    └── requirements.txt         # Python dependencies
```

---

### Comparison with Similar Projects

| Feature | **nature-paper-hub** | Yuan1z0825/nature-skills | Boom5426/Nature-Paper-Skills |
|---------|:---:|:---:|:---:|
| Per-journal word/figure/ref limits (9 journals) | ✅ | ❌ | ✅ |
| Single-entry full-pipeline routing | ✅ | ❌ | ✅ |
| LaTeX / Overleaf template | ✅ | ❌ | ❌ |
| Word export (.docx) | ✅ | ❌ | ❌ |
| matplotlib/R figure code generation | ✅ | ✅ | ❌ |
| CSV/Excel → auto figure | ✅ | ❌ | ❌ |
| Bilingual paper reader | ✅ | ✅ | ❌ |
| Paper to PPTX presentation | ✅ | ✅ | ❌ |
| Multi-format citation export (BibTeX/RIS/ENW/Zotero) | ✅ | ✅ | ❌ |
| CrossRef API real-time citation verification | ✅ | ❌ | ❌ |
| RetractionWatch retraction check | ✅ | ❌ | ❌ |
| RAG writing style grounded in personal library | ✅ | ❌ | ❌ |
| Personal literature library integration | ✅ | ❌ | ❌ |
| Claude Code plugin | ✅ | ✅ | ✅ |
| Codex compatible | ✅ | ✅ | ✅ |
| OpenClaw compatible | ✅ | ❌ | ❌ |

---

### Acknowledgements

Inspired in part by:
- [Yuan1z0825/nature-skills](https://github.com/Yuan1z0825/nature-skills) — Yuan Yizhe, Shanghai Jiao Tong University
- [Boom5426/Nature-Paper-Skills](https://github.com/Boom5426/Nature-Paper-Skills)
- [Nature Portfolio Author Guidelines](https://www.nature.com/authors)

---

### License

MIT License — free to use, modify, and distribute with attribution.

---

### Contributing

Issues and PRs are welcome. Please include:
1. Target journal
2. Feature gap or bug description
3. Suggested implementation approach


path: scripts/auto_figure.py
content: #!/usr/bin/env python3
"""
auto_figure.py — Nature-style publication figure generator.

Usage:
    python3 auto_figure.py --input data.csv --output figure.pdf [options]
"""

import argparse
import math
import os
import sys
import warnings
from datetime import datetime
from pathlib import Path

# ── Dependencies ──────────────────────────────────────────────────────────────
try:
    import pandas as pd
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    import matplotlib.font_manager as fm
    import seaborn as sns
except ImportError as e:
    sys.exit(f"❌ Missing dependency: {e}\n"
             "Run: pip3 install pandas matplotlib seaborn openpyxl -q --break-system-packages")

# ── Palettes ──────────────────────────────────────────────────────────────────
PALETTES = {
    'nature':     ['#4878CF','#D65F5F','#6ACC65','#EE854A','#956CB4','#82C6E2','#D5BB67','#8C8C8C'],
    'colorblind': ['#0072B2','#E69F00','#009E73','#CC79A7','#56B4E9','#F0E442','#D55E00','#000000'],
    'grayscale':  ['#000000','#404040','#808080','#AAAAAA','#CCCCCC'],
}

# ── Width presets (mm → inches at 300 DPI) ────────────────────────────────────
MM_TO_IN = 1 / 25.4
WIDTH_MM = {'single': 89, 'onehalf': 120, 'double': 183}

# ── Nature rcParams ───────────────────────────────────────────────────────────
NATURE_RC = {
    'font.family':        'Arial',
    'font.size':          8,
    'axes.linewidth':     0.8,
    'axes.spines.top':    False,
    'axes.spines.right':  False,
    'xtick.major.width':  0.8,
    'ytick.major.width':  0.8,
    'xtick.major.size':   3,
    'ytick.major.size':   3,
    'xtick.direction':    'out',
    'ytick.direction':    'out',
    'figure.dpi':         300,
    'savefig.dpi':        300,
    'savefig.bbox':       'tight',
    'savefig.pad_inches': 0.05,
    'pdf.fonttype':       42,
    'ps.fonttype':        42,
}


# ── Font check ────────────────────────────────────────────────────────────────
def _setup_font(lang: str) -> None:
    """Apply Nature rcParams; fall back to DejaVu Sans if Arial missing."""
    available = {f.name for f in fm.fontManager.ttflist}
    if lang == 'zh':
        # prefer CJK fonts for Chinese labels
        cjk_prefs = ['Arial Unicode MS', 'PingFang SC', 'Heiti SC',
                     'STHeiti', 'Microsoft YaHei', 'SimHei']
        chosen = next((f for f in cjk_prefs if f in available), None)
        if chosen:
            NATURE_RC['font.family'] = chosen
        else:
            warnings.warn("⚠️  No CJK font found — Chinese labels may not render correctly.")
    else:
        if 'Arial' not in available:
            warnings.warn("⚠️  Arial not found — falling back to DejaVu Sans.")
            NATURE_RC['font.family'] = 'DejaVu Sans'
    plt.rcParams.update(NATURE_RC)


# ── Data loading ──────────────────────────────────────────────────────────────
def load_data(path: str) -> pd.DataFrame:
    p = Path(path)
    suffix = p.suffix.lower()
    if suffix == '.csv':
        df = pd.read_csv(p)
    elif suffix in ('.xls', '.xlsx'):
        df = pd.read_excel(p)
    elif suffix == '.json':
        df = pd.read_json(p)
    else:
        sys.exit(f"❌ Unsupported file type: {suffix}  (use .csv, .xlsx, or .json)")
    # NaN handling
    n_nan = df.isna().sum().sum()
    if n_nan:
        warnings.warn(f"⚠️  Dropped {n_nan} NaN value(s) from data.")
        df = df.dropna()
    return df


# ── Column classification ─────────────────────────────────────────────────────
def classify_columns(df: pd.DataFrame):
    numeric_cols = df.select_dtypes(include='number').columns.tolist()
    datetime_cols = df.select_dtypes(include=['datetime', 'datetimetz']).columns.tolist()
    # also try parsing object cols as datetime
    for col in df.select_dtypes(include='object').columns:
        try:
            pd.to_datetime(df[col])
            datetime_cols.append(col)
        except Exception:
            pass
    categorical_cols = [c for c in df.columns
                        if c not in numeric_cols and c not in datetime_cols]
    return numeric_cols, categorical_cols, datetime_cols


# ── Auto-detect chart type ────────────────────────────────────────────────────
def auto_detect_type(df: pd.DataFrame, numeric_cols, categorical_cols, datetime_cols) -> str:
    n_num = len(numeric_cols)
    n_cat = len(categorical_cols)
    n_dt  = len(datetime_cols)

    if n_dt >= 1 and n_num >= 1:
        return 'line'
    if n_cat == 1 and n_num == 1:
        return 'bar'
    if n_num >= 2:
        # heuristic: square-ish all-numeric frame → heatmap
        if n_cat == 0 and n_dt == 0 and abs(len(df) - n_num) <= max(2, n_num * 0.3):
            return 'heatmap'
        return 'scatter'
    if n_cat >= 1 and n_num >= 2:
        return 'box'
    return 'bar'  # fallback


# ── Panel layout helper ────────────────────────────────────────────────────────
def panel_layout(n: int):
    if n <= 3:
        return 1, n
    if n == 4:
        return 2, 2
    return math.ceil(n / 3), 3


# ── Figure width/height ───────────────────────────────────────────────────────
def figure_size(width_key: str, n_rows: int, n_cols: int):
    w_mm = WIDTH_MM.get(width_key, 183)
    w_in = w_mm * MM_TO_IN
    h_in = w_in * (n_rows / n_cols) * 0.75
    h_in = max(h_in, 1.5)  # minimum height
    return w_in, h_in, w_mm


# ── Panel label helper ────────────────────────────────────────────────────────
def add_panel_label(ax, index: int):
    label = chr(ord('a') + index)
    ax.text(-0.15, 1.05, label, transform=ax.transAxes,
            fontsize=8, fontweight='bold', va='top', ha='right')


# ── Guard group column ────────────────────────────────────────────────────────
def guard_group_col(df: pd.DataFrame, group_col: str) -> pd.DataFrame:
    if group_col and group_col in df.columns:
        cats = df[group_col].nunique()
        if cats > 10:
            warnings.warn(f"⚠️  Group column '{group_col}' has {cats} categories — "
                          "using only top 10.")
            top10 = df[group_col].value_counts().nlargest(10).index
            df = df[df[group_col].isin(top10)].copy()
    return df


# ── Plot functions ────────────────────────────────────────────────────────────

def _set_labels(ax, xlabel, ylabel):
    if xlabel:
        ax.set_xlabel(xlabel)
    if ylabel:
        ax.set_ylabel(ylabel)


def plot_line(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title):
    colors = palette
    if group_col and group_col in df.columns:
        for i, (grp, sub) in enumerate(df.groupby(group_col)):
            ax.plot(sub[x_col], sub[y_col], label=str(grp),
                    color=colors[i % len(colors)], linewidth=1)
        ax.legend(title=title or group_col, frameon=False, fontsize=7)
    else:
        ax.plot(df[x_col], df[y_col], color=colors[0], linewidth=1,
                label=title or y_col)
        if title:
            ax.legend(frameon=False, fontsize=7)
    _set_labels(ax, xlabel or x_col, ylabel or y_col)


def plot_bar(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title):
    colors = palette
    if group_col and group_col in df.columns:
        pivot = df.groupby([x_col, group_col])[y_col].mean().unstack(group_col)
        pivot.plot(kind='bar', ax=ax, color=colors[:len(pivot.columns)],
                   edgecolor='none', width=0.7)
        ax.legend(title=title or group_col, frameon=False, fontsize=7,
                  bbox_to_anchor=(1, 1), loc='upper left')
    else:
        grp = df.groupby(x_col)[y_col].mean()
        ax.bar(range(len(grp)), grp.values,
               color=colors[:len(grp)], edgecolor='none', width=0.7)
        ax.set_xticks(range(len(grp)))
        ax.set_xticklabels(grp.index, rotation=45, ha='right')
    _set_labels(ax, xlabel or x_col, ylabel or y_col)


def plot_scatter(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title):
    colors = palette
    if group_col and group_col in df.columns:
        for i, (grp, sub) in enumerate(df.groupby(group_col)):
            ax.scatter(sub[x_col], sub[y_col], label=str(grp),
                       color=colors[i % len(colors)], s=12, alpha=0.8, linewidths=0)
        ax.legend(title=title or group_col, frameon=False, fontsize=7)
    else:
        ax.scatter(df[x_col], df[y_col], color=colors[0], s=12, alpha=0.8, linewidths=0)
    _set_labels(ax, xlabel or x_col, ylabel or y_col)


def plot_heatmap(ax, df, palette, xlabel, ylabel, title):
    import numpy as np
    numeric_df = df.select_dtypes(include='number')
    sns.heatmap(numeric_df, ax=ax, cmap='viridis', linewidths=0,
                cbar_kws={'shrink': 0.8, 'pad': 0.02})
    _set_labels(ax, xlabel or '', ylabel or '')
    if title:
        ax.set_title(title, fontsize=8)


def plot_box(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title, kind='box'):
    colors = palette
    kwargs = dict(
        data=df, x=x_col, y=y_col,
        hue=group_col if group_col and group_col in df.columns else None,
        ax=ax, palette=colors[:df[x_col].nunique()] if x_col else None,
    )
    if kind == 'violin':
        sns.violinplot(**kwargs, linewidth=0.8, inner='box', cut=0)
    else:
        sns.boxplot(**kwargs, linewidth=0.8, flierprops=dict(marker='o', markersize=2))
    _set_labels(ax, xlabel or x_col, ylabel or y_col)
    if group_col and group_col in df.columns:
        ax.legend(title=title or group_col, frameon=False, fontsize=7)
    elif title:
        ax.set_title(title, fontsize=8)


# ── Multi-panel rendering ─────────────────────────────────────────────────────

def render_panels(df, panel_cols, x_col, group_col, chart_type,
                  palette, xlabel, ylabel, title, width_key, lang):
    n = len(panel_cols)
    n_rows, n_cols = panel_layout(n)
    w_in, h_in, w_mm = figure_size(width_key, n_rows, n_cols)
    fig, axes = plt.subplots(n_rows, n_cols,
                             figsize=(w_in, h_in),
                             squeeze=False)
    axes_flat = [axes[r][c] for r in range(n_rows) for c in range(n_cols)]

    for i, ycol in enumerate(panel_cols):
        ax = axes_flat[i]
        _draw_single(ax, df, x_col, ycol, group_col, chart_type,
                     palette, xlabel, ylabel or ycol, title)
        add_panel_label(ax, i)

    # hide unused axes
    for j in range(n, len(axes_flat)):
        axes_flat[j].set_visible(False)

    fig.tight_layout()
    return fig, n_rows, n_cols, w_mm, h_in


def _draw_single(ax, df, x_col, y_col, group_col, chart_type,
                 palette, xlabel, ylabel, title):
    if chart_type == 'line':
        plot_line(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title)
    elif chart_type == 'bar':
        plot_bar(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title)
    elif chart_type == 'scatter':
        plot_scatter(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title)
    elif chart_type == 'heatmap':
        plot_heatmap(ax, df, palette, xlabel, ylabel, title)
    elif chart_type == 'box':
        plot_box(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title, kind='box')
    elif chart_type == 'violin':
        plot_box(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title, kind='violin')
    else:
        plot_bar(ax, df, x_col, y_col, group_col, palette, xlabel, ylabel, title)


# ── Main ──────────────────────────────────────────────────────────────────────

def parse_args(argv=None):
    p = argparse.ArgumentParser(description='Generate Nature-style publication figures.')
    p.add_argument('--input',     required=True,  help='Input data file (.csv, .xlsx, .json)')
    p.add_argument('--output',    default=None,   help='Output PDF path (default: ~/Downloads/auto_figure_YYYYMMDD.pdf)')
    p.add_argument('--type',      default='auto',
                   choices=['auto','line','bar','scatter','heatmap','box','violin'],
                   help='Chart type (default: auto)')
    p.add_argument('--title',     default='',     help='Figure title (appears in legend)')
    p.add_argument('--xlabel',    default='',     help='X-axis label')
    p.add_argument('--ylabel',    default='',     help='Y-axis label')
    p.add_argument('--group-col', default=None,   dest='group_col', help='Grouping/hue column')
    p.add_argument('--x-col',     default=None,   dest='x_col',     help='X column name')
    p.add_argument('--y-col',     default=None,   dest='y_col',     help='Y column name')
    p.add_argument('--width',     default='double',
                   choices=['single','onehalf','double'],
                   help='Column width preset (default: double)')
    p.add_argument('--panels',    default=None,
                   help='Comma-separated y-columns for multi-panel layout')
    p.add_argument('--palette',   default='nature',
                   choices=['nature','colorblind','grayscale'],
                   help='Color palette (default: nature)')
    p.add_argument('--lang',      default='en', choices=['en','zh'],
                   help='Language for font handling (default: en)')
    return p.parse_args(argv)


def main(argv=None):
    args = parse_args(argv)

    # Font setup
    _setup_font(args.lang)

    # Output path
    if args.output is None:
        date_str = datetime.now().strftime('%Y%m%d')
        args.output = str(Path.home() / 'Downloads' / f'auto_figure_{date_str}.pdf')

    out_path = Path(args.output)
    out_pdf  = out_path.with_suffix('.pdf')
    out_png  = out_path.with_suffix('.png')
    out_pdf.parent.mkdir(parents=True, exist_ok=True)

    palette  = PALETTES[args.palette]

    # Load data
    df = load_data(args.input)

    # Guard group column
    if args.group_col:
        df = guard_group_col(df, args.group_col)

    # Classify columns
    numeric_cols, categorical_cols, datetime_cols = classify_columns(df)

    # Detect chart type
    chart_type = args.type
    detected   = False
    if chart_type == 'auto':
        chart_type = auto_detect_type(df, numeric_cols, categorical_cols, datetime_cols)
        detected   = True

    # Resolve x/y columns
    x_col = args.x_col
    y_col = args.y_col

    if x_col is None:
        if datetime_cols:
            x_col = datetime_cols[0]
        elif categorical_cols:
            x_col = categorical_cols[0]
        elif numeric_cols:
            x_col = numeric_cols[0]

    if y_col is None:
        candidates = [c for c in numeric_cols if c != x_col]
        y_col = candidates[0] if candidates else (numeric_cols[0] if numeric_cols else df.columns[-1])

    # ── Multi-panel mode ──────────────────────────────────────────────────────
    panel_cols = None
    if args.panels:
        panel_cols = [c.strip() for c in args.panels.split(',') if c.strip() in df.columns]
        missing = [c.strip() for c in args.panels.split(',') if c.strip() not in df.columns]
        if missing:
            warnings.warn(f"⚠️  Panel columns not found in data: {missing}")

    if panel_cols and len(panel_cols) > 1:
        fig, n_rows, n_cols_layout, w_mm, h_in = render_panels(
            df, panel_cols, x_col, args.group_col, chart_type,
            palette, args.xlabel, args.ylabel, args.title, args.width, args.lang
        )
        n_panels = len(panel_cols)
        panel_labels = ', '.join(chr(ord('a') + i) for i in range(n_panels))
        h_mm = round(h_in / MM_TO_IN)
    else:
        # Single panel
        n_panels = 1
        panel_labels = 'a'
        n_rows, n_cols_layout = 1, 1
        w_mm = WIDTH_MM.get(args.width, 183)
        w_in, h_in, _ = figure_size(args.width, 1, 1)
        h_mm = round(h_in / MM_TO_IN)

        fig, ax = plt.subplots(1, 1, figsize=(w_in, h_in))

        if chart_type == 'heatmap':
            plot_heatmap(ax, df, palette, args.xlabel, args.ylabel, args.title)
        else:
            _draw_single(ax, df, x_col, y_col, args.group_col, chart_type,
                         palette, args.xlabel, args.ylabel, args.title)

        fig.tight_layout()

    # Save
    fig.savefig(out_pdf)
    fig.savefig(out_png)
    plt.close(fig)

    # Summary
    type_str = f"{chart_type} (auto-detected)" if detected else chart_type
    print("✅ Figure generated:")
    print(f"   Type:   {type_str}")
    print(f"   Panels: {n_panels} ({panel_labels})" if n_panels > 1 else f"   Panels: 1 (a)")
    print(f"   Size:   {w_mm}mm × {h_mm}mm ({args.width} column)")
    print(f"   Files:  {out_pdf}, {out_png}")

    return str(out_pdf)


# ── Self-test ─────────────────────────────────────────────────────────────────

def _self_test():
    import tempfile, csv, os

    # Write a small inline CSV
    rows = [
        ['group', 'value', 'score'],
        ['A',      10,      0.5],
        ['B',      20,      0.8],
        ['C',      15,      0.6],
        ['A',      12,      0.55],
        ['B',      18,      0.75],
    ]
    with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as f:
        csv.writer(f).writerows(rows)
        tmp_csv = f.name

    with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
        tmp_pdf = f.name

    try:
        argv = ['--input', tmp_csv, '--output', tmp_pdf]
        result = main(argv)
        assert result and Path(result).exists(), "PDF not created"
        png = Path(result).with_suffix('.png')
        assert png.exists(), "PNG not created"
        print("✅ auto_figure test passed")
    finally:
        for p in [tmp_csv, tmp_pdf,
                  str(Path(tmp_pdf).with_suffix('.pdf')),
                  str(Path(tmp_pdf).with_suffix('.png'))]:
            try:
                os.unlink(p)
            except FileNotFoundError:
                pass


if __name__ == '__main__':
    if '--self-test' in sys.argv:
        _self_test()
    else:
        main()


path: scripts/export_docx.py
content: #!/usr/bin/env python3
"""
export_docx.py — Generate a Nature-portfolio-style Word manuscript from a JSON draft.

Usage:
    python3 export_docx.py --input ~/Downloads/nature-paper-draft.json \
                           --output ~/Downloads/nature-paper-20260517.docx
"""

import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def set_font(run, name="Times New Roman", size_pt=12, bold=False, italic=False,
             color=None):
    run.font.name = name
    run.font.size = Pt(size_pt)
    run.font.bold = bold
    run.font.italic = italic
    if color:
        run.font.color.rgb = RGBColor(*color)


def set_paragraph_format(para, space_before=0, space_after=0,
                          line_spacing=None, first_line_indent=None,
                          left_indent=None, alignment=None):
    pf = para.paragraph_format
    pf.space_before = Pt(space_before)
    pf.space_after = Pt(space_after)
    if line_spacing is not None:
        from docx.shared import Pt as _Pt
        pf.line_spacing = _Pt(line_spacing)
    if first_line_indent is not None:
        pf.first_line_indent = Pt(first_line_indent)
    if left_indent is not None:
        pf.left_indent = Pt(left_indent)
    if alignment is not None:
        para.alignment = alignment


def add_page_numbers(doc):
    """Insert a bottom-centre page number field into the default footer."""
    section = doc.sections[0]
    footer = section.footer
    footer.is_linked_to_previous = False
    para = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
    para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    para.clear()

    run = para.add_run()
    fld = OxmlElement("w:fldChar")
    fld.set(qn("w:fldCharType"), "begin")
    run._r.append(fld)

    run2 = para.add_run()
    instrText = OxmlElement("w:instrText")
    instrText.set(qn("xml:space"), "preserve")
    instrText.text = " PAGE "
    run2._r.append(instrText)

    run3 = para.add_run()
    fld2 = OxmlElement("w:fldChar")
    fld2.set(qn("w:fldCharType"), "end")
    run3._r.append(fld2)

    for run in (run, run2, run3):
        set_font(run, size_pt=10)


def add_section_heading(doc, text, level="section"):
    """Add a bold section heading (or italic-bold subsection heading)."""
    para = doc.add_paragraph()
    set_paragraph_format(para, space_before=12, space_after=4, line_spacing=24)
    run = para.add_run(text)
    if level == "section":
        set_font(run, size_pt=12, bold=True)
    else:  # subsection
        set_font(run, size_pt=11, bold=True, italic=True)
    return para


def add_body_paragraph(doc, text, first=False):
    """Add a double-spaced body paragraph."""
    para = doc.add_paragraph()
    indent = 0 if first else 0           # Nature style: no first-line indent
    set_paragraph_format(para, space_before=0, space_after=0,
                          line_spacing=24, first_line_indent=indent)
    run = para.add_run(text)
    set_font(run, size_pt=12)
    return para


# ---------------------------------------------------------------------------
# Main builder
# ---------------------------------------------------------------------------

def build_docx(data: dict, output_path: Path):
    doc = Document()

    # ── Page setup ──────────────────────────────────────────────────────────
    section = doc.sections[0]
    margin = Cm(2.54)
    section.top_margin = margin
    section.bottom_margin = margin
    section.left_margin = margin
    section.right_margin = margin

    # ── Page numbers ────────────────────────────────────────────────────────
    add_page_numbers(doc)

    # ── Header note (line-numbers reminder) ─────────────────────────────────
    note_para = doc.add_paragraph()
    set_paragraph_format(note_para, space_before=0, space_after=6, line_spacing=12)
    note_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    note_run = note_para.add_run(
        "[Note for submission: enable continuous line numbers in your journal portal "
        "before uploading this manuscript.]"
    )
    set_font(note_run, size_pt=9, italic=True, color=(128, 128, 128))

    # ── Journal tag ─────────────────────────────────────────────────────────
    journal = data.get("journal", "Nature Portfolio")
    j_para = doc.add_paragraph()
    set_paragraph_format(j_para, space_before=0, space_after=4, line_spacing=12)
    j_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    j_run = j_para.add_run(journal.upper())
    set_font(j_run, size_pt=9, bold=True, color=(80, 80, 80))

    # ── Title ────────────────────────────────────────────────────────────────
    title_para = doc.add_paragraph()
    set_paragraph_format(title_para, space_before=12, space_after=8, line_spacing=24)
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title_run = title_para.add_run(data.get("title", "Manuscript Title"))
    set_font(title_run, size_pt=14, bold=True)

    # ── Authors ──────────────────────────────────────────────────────────────
    authors = data.get("authors", [])
    auth_para = doc.add_paragraph()
    set_paragraph_format(auth_para, space_before=0, space_after=4, line_spacing=24)
    auth_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    auth_run = auth_para.add_run(", ".join(authors))
    set_font(auth_run, size_pt=11)

    # ── Affiliations ─────────────────────────────────────────────────────────
    for i, aff in enumerate(data.get("affiliations", []), 1):
        aff_para = doc.add_paragraph()
        set_paragraph_format(aff_para, space_before=0, space_after=2, line_spacing=14)
        aff_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        label = f"{i}. " if len(data.get("affiliations", [])) > 1 else ""
        aff_run = aff_para.add_run(f"{label}{aff}")
        set_font(aff_run, size_pt=9, italic=True)

    # ── Corresponding email ──────────────────────────────────────────────────
    email = data.get("corresponding_email", "")
    if email:
        em_para = doc.add_paragraph()
        set_paragraph_format(em_para, space_before=2, space_after=8, line_spacing=14)
        em_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
        em_run = em_para.add_run(f"Correspondence: {email}")
        set_font(em_run, size_pt=9, italic=True)

    doc.add_paragraph()  # visual spacer

    # ── Abstract ─────────────────────────────────────────────────────────────
    abs_para = doc.add_paragraph()
    set_paragraph_format(abs_para, space_before=0, space_after=2, line_spacing=14)
    abs_label = abs_para.add_run("Abstract  ")
    set_font(abs_label, size_pt=12, bold=True)
    abs_body = abs_para.add_run(data.get("abstract", ""))
    set_font(abs_body, size_pt=12)

    # ── Keywords ─────────────────────────────────────────────────────────────
    keywords = data.get("keywords", [])
    if keywords:
        kw_para = doc.add_paragraph()
        set_paragraph_format(kw_para, space_before=4, space_after=8, line_spacing=14)
        kw_label = kw_para.add_run("Keywords: ")
        set_font(kw_label, size_pt=10, bold=True)
        kw_body = kw_para.add_run("; ".join(keywords))
        set_font(kw_body, size_pt=10)

    # ── Introduction ─────────────────────────────────────────────────────────
    add_section_heading(doc, "Introduction")
    intro = data.get("introduction", "")
    if intro:
        for i, para_text in enumerate(intro.split("\n\n")):
            para_text = para_text.strip()
            if para_text:
                add_body_paragraph(doc, para_text, first=(i == 0))

    # ── Results ──────────────────────────────────────────────────────────────
    add_section_heading(doc, "Results")
    for subsec in data.get("results", []):
        add_section_heading(doc, subsec.get("title", ""), level="subsection")
        content = subsec.get("content", "")
        for i, para_text in enumerate(content.split("\n\n")):
            para_text = para_text.strip()
            if para_text:
                add_body_paragraph(doc, para_text, first=(i == 0))

    # ── Discussion ────────────────────────────────────────────────────────────
    add_section_heading(doc, "Discussion")
    discussion = data.get("discussion", "")
    if discussion:
        for i, para_text in enumerate(discussion.split("\n\n")):
            para_text = para_text.strip()
            if para_text:
                add_body_paragraph(doc, para_text, first=(i == 0))

    # ── Methods ───────────────────────────────────────────────────────────────
    add_section_heading(doc, "Methods")
    for subsec in data.get("methods", []):
        add_section_heading(doc, subsec.get("title", ""), level="subsection")
        content = subsec.get("content", "")
        for i, para_text in enumerate(content.split("\n\n")):
            para_text = para_text.strip()
            if para_text:
                add_body_paragraph(doc, para_text, first=(i == 0))

    # ── Data Availability ────────────────────────────────────────────────────
    da = data.get("data_availability", "")
    if da:
        add_section_heading(doc, "Data Availability")
        add_body_paragraph(doc, da)

    # ── Author Contributions ─────────────────────────────────────────────────
    ac = data.get("author_contributions", "")
    if ac:
        add_section_heading(doc, "Author Contributions")
        add_body_paragraph(doc, ac)

    # ── Competing Interests ───────────────────────────────────────────────────
    ci = data.get("competing_interests", "")
    if ci:
        add_section_heading(doc, "Competing Interests")
        add_body_paragraph(doc, ci)

    # ── Acknowledgements ──────────────────────────────────────────────────────
    ack = data.get("acknowledgements", "")
    if ack:
        add_section_heading(doc, "Acknowledgements")
        add_body_paragraph(doc, ack)

    # ── References ────────────────────────────────────────────────────────────
    refs = data.get("references", [])
    if refs:
        add_section_heading(doc, "References")
        for ref_text in refs:
            ref_para = doc.add_paragraph()
            # Hanging indent: left_indent=18pt, first_line_indent=-18pt
            pf = ref_para.paragraph_format
            pf.left_indent = Pt(18)
            pf.first_line_indent = Pt(-18)
            pf.space_before = Pt(2)
            pf.space_after = Pt(2)
            pf.line_spacing = Pt(12)
            ref_run = ref_para.add_run(ref_text)
            set_font(ref_run, size_pt=10)

    # ── Figure Legends ────────────────────────────────────────────────────────
    fig_legends = data.get("figure_legends", [])
    if fig_legends:
        add_section_heading(doc, "Figure Legends")
        for legend in fig_legends:
            leg_para = doc.add_paragraph()
            set_paragraph_format(leg_para, space_before=4, space_after=4, line_spacing=14)

            # Bold "Figure X |" prefix  (split on first " " after the pipe)
            if "|" in legend:
                prefix, rest = legend.split("|", 1)
                prefix_run = leg_para.add_run(prefix.rstrip() + " |")
                set_font(prefix_run, size_pt=10, bold=True)
                body_run = leg_para.add_run(rest)
                set_font(body_run, size_pt=10)
            else:
                run = leg_para.add_run(legend)
                set_font(run, size_pt=10)

    # ── Save ──────────────────────────────────────────────────────────────────
    output_path.parent.mkdir(parents=True, exist_ok=True)
    doc.save(str(output_path))


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------

def parse_args():
    today = datetime.today().strftime("%Y%m%d")
    parser = argparse.ArgumentParser(
        description="Generate a Nature-portfolio-style Word manuscript from a JSON draft."
    )
    parser.add_argument(
        "--input",
        default=str(Path("~/Downloads/nature-paper-draft.json").expanduser()),
        help="Path to the input JSON file.",
    )
    parser.add_argument(
        "--output",
        default=str(Path(f"~/Downloads/nature-paper-{today}.docx").expanduser()),
        help="Path for the output .docx file.",
    )
    return parser.parse_args()


def main():
    args = parse_args()
    input_path = Path(args.input).expanduser().resolve()
    output_path = Path(args.output).expanduser().resolve()

    if not input_path.exists():
        print(f"❌ Input file not found: {input_path}", file=sys.stderr)
        sys.exit(1)

    with input_path.open("r", encoding="utf-8") as fh:
        data = json.load(fh)

    build_docx(data, output_path)
    print(f"✅ Export successful → {output_path}")


if __name__ == "__main__":
    main()


path: scripts/export_pptx.py
content: #!/usr/bin/env python3
"""
export_pptx.py — Generate a clean scientific presentation PPTX from a JSON input.
Usage:
    python3 export_pptx.py --input paper.json --output slides.pptx
"""

import argparse
import json
import os
import sys
from datetime import datetime
from pathlib import Path

from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Cm, Pt

# ── Palette ──────────────────────────────────────────────────────────────────
NAVY   = RGBColor(0x0E, 0x4D, 0x92)
WHITE  = RGBColor(0xFF, 0xFF, 0xFF)
DARK_GRAY = RGBColor(0x33, 0x33, 0x33)
LIGHT_GRAY = RGBColor(0xCC, 0xCC, 0xCC)

# ── Slide dimensions: 16:9 widescreen ────────────────────────────────────────
SLIDE_W = Cm(33.87)
SLIDE_H = Cm(19.05)

# ── Layout constants ──────────────────────────────────────────────────────────
TITLE_BAR_H    = Cm(2.8)
MARGIN         = Cm(1.2)
SLIDE_NUM_W    = Cm(1.5)
SLIDE_NUM_H    = Cm(0.6)
BODY_TOP       = TITLE_BAR_H + Cm(0.3)
BODY_H         = SLIDE_H - BODY_TOP - Cm(1.0)


def _blank_slide(prs: Presentation) -> object:
    """Add a blank slide (layout 6) to the presentation."""
    blank_layout = prs.slide_layouts[6]  # "Blank" layout
    return prs.slides.add_slide(blank_layout)


def _set_background_white(slide) -> None:
    """Ensure slide background is white (already default for blank layout)."""
    fill = slide.background.fill
    fill.solid()
    fill.fore_color.rgb = WHITE


def _add_title_bar(slide, title_text: str) -> None:
    """Draw the navy title bar at the top and place title text inside it."""
    # Navy rectangle
    bar = slide.shapes.add_shape(
        1,  # MSO_SHAPE_TYPE.RECTANGLE
        0, 0, SLIDE_W, TITLE_BAR_H
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = NAVY
    bar.line.fill.background()  # no border

    # Title text box on top of bar
    txb = slide.shapes.add_textbox(MARGIN, Cm(0.2), SLIDE_W - 2 * MARGIN, TITLE_BAR_H - Cm(0.4))
    tf = txb.text_frame
    tf.word_wrap = False
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    run = p.add_run()
    run.text = title_text
    run.font.bold = True
    run.font.size = Pt(28)
    run.font.color.rgb = WHITE


def _add_slide_number(slide, num: int) -> None:
    """Add slide number at bottom right."""
    left = SLIDE_W - MARGIN - SLIDE_NUM_W
    top  = SLIDE_H - Cm(0.8)
    txb = slide.shapes.add_textbox(left, top, SLIDE_NUM_W, SLIDE_NUM_H)
    tf = txb.text_frame
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.RIGHT
    run = p.add_run()
    run.text = str(num)
    run.font.size = Pt(10)
    run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)


def _add_body_textbox(slide, text: str, left=None, top=None, width=None, height=None,
                      font_size=Pt(20), align=PP_ALIGN.LEFT, bold=False, color=DARK_GRAY):
    """Generic helper: add a text box with styled content."""
    left   = left   if left   is not None else MARGIN
    top    = top    if top    is not None else BODY_TOP
    width  = width  if width  is not None else SLIDE_W - 2 * MARGIN
    height = height if height is not None else BODY_H

    txb = slide.shapes.add_textbox(left, top, width, height)
    tf = txb.text_frame
    tf.word_wrap = True

    first = True
    for line in text.split("\n"):
        if first:
            p = tf.paragraphs[0]
            first = False
        else:
            p = tf.add_paragraph()
        p.alignment = align
        run = p.add_run()
        run.text = line
        run.font.size = font_size
        run.font.bold = bold
        run.font.color.rgb = color
        # Indent bullet lines (those starting with • or -)
        if line.strip().startswith(("•", "-", "·")):
            p.level = 1


def _add_figure_placeholder(slide, left, top, width, height, label: str = "Insert Figure here"):
    """Gray rectangle with centered label text."""
    box = slide.shapes.add_shape(1, left, top, width, height)
    box.fill.solid()
    box.fill.fore_color.rgb = LIGHT_GRAY
    box.line.color.rgb = RGBColor(0xAA, 0xAA, 0xAA)

    txb = slide.shapes.add_textbox(left, top + height // 2 - Cm(0.5), width, Cm(1.0))
    tf = txb.text_frame
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.CENTER
    run = p.add_run()
    run.text = label
    run.font.size = Pt(14)
    run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
    run.font.italic = True


# ── Slide builders ────────────────────────────────────────────────────────────

def build_title_slide(prs, data: dict, slide_num: int) -> None:
    slide = _blank_slide(prs)
    _set_background_white(slide)

    # Full-width navy bar (taller for title slide)
    bar_h = Cm(5.5)
    bar = slide.shapes.add_shape(1, 0, Cm(5.5), SLIDE_W, bar_h)
    bar.fill.solid()
    bar.fill.fore_color.rgb = NAVY
    bar.line.fill.background()

    # Paper title
    txb = slide.shapes.add_textbox(MARGIN, Cm(5.8), SLIDE_W - 2 * MARGIN, Cm(4.5))
    tf = txb.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.CENTER
    run = p.add_run()
    run.text = data.get("title", "Untitled")
    run.font.bold = True
    run.font.size = Pt(32)
    run.font.color.rgb = WHITE

    # Authors
    authors = ", ".join(data.get("authors", []))
    txb2 = slide.shapes.add_textbox(MARGIN, Cm(11.5), SLIDE_W - 2 * MARGIN, Cm(1.2))
    tf2 = txb2.text_frame
    p2 = tf2.paragraphs[0]
    p2.alignment = PP_ALIGN.CENTER
    run2 = p2.add_run()
    run2.text = authors
    run2.font.size = Pt(18)
    run2.font.color.rgb = DARK_GRAY

    # Journal / Year / DOI
    journal = data.get("journal", "")
    year    = data.get("year", "")
    doi     = data.get("doi", "")
    meta    = f"{journal}  •  {year}"
    if doi:
        meta += f"  |  DOI: {doi}"
    txb3 = slide.shapes.add_textbox(MARGIN, Cm(13.0), SLIDE_W - 2 * MARGIN, Cm(1.0))
    tf3 = txb3.text_frame
    p3 = tf3.paragraphs[0]
    p3.alignment = PP_ALIGN.CENTER
    run3 = p3.add_run()
    run3.text = meta
    run3.font.size = Pt(14)
    run3.font.color.rgb = RGBColor(0x55, 0x55, 0x55)
    run3.font.italic = True

    _add_slide_number(slide, slide_num)


def build_background_slide(prs, slide_data: dict, slide_num: int) -> None:
    slide = _blank_slide(prs)
    _set_background_white(slide)
    title_text = slide_data.get("title", "Background / Introduction")
    _add_title_bar(slide, title_text)
    content = slide_data.get("content", "")
    _add_body_textbox(slide, content, font_size=Pt(20), color=DARK_GRAY)
    _add_slide_number(slide, slide_num)


def build_results_slide(prs, slide_data: dict, slide_num: int, fig_index: int) -> None:
    slide = _blank_slide(prs)
    _set_background_white(slide)
    title_text = slide_data.get("title", "Results")
    _add_title_bar(slide, title_text)

    half_w = (SLIDE_W - 3 * MARGIN) // 2

    # Left: text content
    content = slide_data.get("content", "")
    _add_body_textbox(slide, content,
                      left=MARGIN, top=BODY_TOP,
                      width=half_w, height=BODY_H,
                      font_size=Pt(18), color=DARK_GRAY)

    # Right: figure placeholder
    fig_label = slide_data.get("figure", f"fig{fig_index}.png")
    placeholder_label = f"Insert Figure {fig_index} here\n({fig_label})"
    fig_left = MARGIN + half_w + MARGIN
    fig_top  = BODY_TOP + Cm(0.3)
    fig_h    = BODY_H - Cm(0.6)
    _add_figure_placeholder(slide, fig_left, fig_top, half_w, fig_h, placeholder_label)

    _add_slide_number(slide, slide_num)


def build_conclusion_slide(prs, slide_data: dict, slide_num: int) -> None:
    slide = _blank_slide(prs)
    _set_background_white(slide)
    title_text = slide_data.get("title", "Conclusion")
    _add_title_bar(slide, title_text)
    content = slide_data.get("content", "")
    _add_body_textbox(slide, content, font_size=Pt(20), color=DARK_GRAY)
    _add_slide_number(slide, slide_num)


def build_generic_slide(prs, slide_data: dict, slide_num: int) -> None:
    slide = _blank_slide(prs)
    _set_background_white(slide)
    title_text = slide_data.get("title", slide_data.get("type", "Slide").capitalize())
    _add_title_bar(slide, title_text)
    content = slide_data.get("content", "")
    _add_body_textbox(slide, content, font_size=Pt(20), color=DARK_GRAY)
    _add_slide_number(slide, slide_num)


# ── Main builder ──────────────────────────────────────────────────────────────

def build_presentation(data: dict, output_path: Path) -> None:
    prs = Presentation()
    prs.slide_width  = SLIDE_W
    prs.slide_height = SLIDE_H

    slides_spec = data.get("slides", [])
    # Ensure at least a title slide if none specified
    if not slides_spec:
        slides_spec = [{"type": "title"}]

    slide_num   = 1
    fig_counter = 1

    for spec in slides_spec:
        stype = spec.get("type", "generic").lower()

        if stype == "title":
            build_title_slide(prs, data, slide_num)
        elif stype in ("background", "introduction", "intro"):
            build_background_slide(prs, spec, slide_num)
        elif stype in ("results", "result", "methods", "method", "discussion"):
            build_results_slide(prs, spec, slide_num, fig_counter)
            if spec.get("figure"):
                fig_counter += 1
        elif stype in ("conclusion", "conclusions", "summary"):
            build_conclusion_slide(prs, spec, slide_num)
        else:
            build_generic_slide(prs, spec, slide_num)

        slide_num += 1

    output_path.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(output_path))


# ── CLI ───────────────────────────────────────────────────────────────────────

def parse_args():
    today = datetime.now().strftime("%Y%m%d")
    default_input  = Path.home() / "Downloads" / "nature-paper-draft.json"
    default_output = Path.home() / "Downloads" / f"paper-slides-{today}.pptx"

    parser = argparse.ArgumentParser(
        description="Generate a scientific PPTX presentation from a Nature paper JSON draft."
    )
    parser.add_argument("--input",  default=str(default_input),
                        help=f"Path to input JSON (default: {default_input})")
    parser.add_argument("--output", default=str(default_output),
                        help=f"Path to output PPTX (default: {default_output})")
    return parser.parse_args()


def main():
    args = parse_args()
    input_path  = Path(args.input).expanduser()
    output_path = Path(args.output).expanduser()

    if not input_path.exists():
        print(f"❌ Input file not found: {input_path}", file=sys.stderr)
        sys.exit(1)

    with open(input_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    build_presentation(data, output_path)
    print(f"✅ PPT export successful: {output_path}")


if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: python-docx
python-pptx
pandas
matplotlib
seaborn
openpyxl


path: templates/journal-specs.json
content: {
  "journals": {
    "nature": {
      "name": "Nature",
      "url": "https://www.nature.com/nature/for-authors",
      "impact_factor": 63.7,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 30,
          "methods_words": 3000,
          "methods_location": "after_references",
          "notes": "Methods sits after references; Extended Data up to 10 items"
        },
        "letter": {
          "body_words": 1500,
          "abstract_words": 150,
          "figures": 4,
          "references": 30
        }
      },
      "style": "numbered",
      "template_search": "site:nature.com filetype:pdf OR arxiv.org nature journal article"
    },
    "nature_materials": {
      "name": "Nature Materials",
      "url": "https://www.nature.com/nmat/for-authors",
      "impact_factor": 37.2,
      "acceptance_rate": "~9%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 50,
          "methods_words": 3000,
          "methods_location": "after_references"
        },
        "letter": {
          "body_words": 1500,
          "abstract_words": 150,
          "figures": 4,
          "references": 30
        }
      },
      "style": "numbered",
      "scope": "Materials science: synthesis, characterization, properties, applications"
    },
    "nature_chemistry": {
      "name": "Nature Chemistry",
      "url": "https://www.nature.com/nchem/for-authors",
      "impact_factor": 19.2,
      "acceptance_rate": "~9%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 50,
          "methods_words": 3000,
          "methods_location": "after_references"
        },
        "letter": {
          "body_words": 1500,
          "abstract_words": 150,
          "figures": 4,
          "references": 30
        }
      },
      "style": "numbered",
      "scope": "Chemical research across all disciplines"
    },
    "nature_energy": {
      "name": "Nature Energy",
      "url": "https://www.nature.com/nenergy/for-authors",
      "impact_factor": 60.9,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 50,
          "methods_words": 3000,
          "methods_location": "after_references"
        }
      },
      "style": "numbered",
      "scope": "Energy generation, distribution, storage, conversion, efficiency, policy"
    },
    "nature_catalysis": {
      "name": "Nature Catalysis",
      "url": "https://www.nature.com/natcatal/for-authors",
      "impact_factor": 37.8,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 50,
          "methods_words": 3000,
          "methods_location": "after_references"
        }
      },
      "style": "numbered",
      "scope": "Catalysis: heterogeneous, homogeneous, enzymatic, electrocatalysis, photocatalysis"
    },
    "nature_sustainability": {
      "name": "Nature Sustainability",
      "url": "https://www.nature.com/natsustain/for-authors",
      "impact_factor": 25.1,
      "acceptance_rate": "~10%",
      "article_types": {
        "article": {
          "body_words": 4000,
          "abstract_words": 200,
          "figures": 8,
          "references": 60
        }
      },
      "style": "numbered",
      "scope": "Sustainability science: environment, society, economics"
    },
    "nature_communications": {
      "name": "Nature Communications",
      "url": "https://www.nature.com/ncomms/for-authors",
      "impact_factor": 15.7,
      "acceptance_rate": "~20%",
      "open_access": true,
      "article_types": {
        "article": {
          "body_words": 5000,
          "body_includes_methods": true,
          "abstract_words": 200,
          "figures": 10,
          "references": 60,
          "methods_location": "within_main_text"
        }
      },
      "style": "numbered",
      "scope": "All natural sciences — most flexible Nature journal"
    },
    "nature_methods": {
      "name": "Nature Methods",
      "url": "https://www.nature.com/nmeth/for-authors",
      "impact_factor": 32.1,
      "acceptance_rate": "~8-10%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "abstract_words": 150,
          "figures": 6,
          "references": 50,
          "methods_words": 3000,
          "methods_location": "after_references",
          "notes": "Methods description lives in Online Methods after refs"
        }
      },
      "style": "numbered",
      "scope": "Novel experimental/computational methods for life sciences"
    },
    "nature_computational_science": {
      "name": "Nature Computational Science",
      "url": "https://www.nature.com/natcomputsci/for-authors",
      "impact_factor": 12.0,
      "acceptance_rate": "~12%",
      "article_types": {
        "article": {
          "body_words": 5000,
          "abstract_words": 200,
          "figures": 8,
          "references": 60
        }
      },
      "style": "numbered",
      "scope": "Algorithms, software, computational approaches across all sciences"
    },

    "nature_chemical_engineering": {
      "name": "Nature Chemical Engineering",
      "url": "https://www.nature.com/natchemeng/for-authors",
      "impact_factor": 13.0,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3500,
          "body_excludes": "abstract, Methods, references, figure legends",
          "abstract_words": 150,
          "abstract_unreferenced": true,
          "figures": 6,
          "extended_data": 10,
          "references": 50,
          "methods_location": "online_methods_after_discussion",
          "methods_words": null,
          "structure": ["Introduction (no heading)", "Results", "Discussion or Conclusions", "Online Methods"],
          "notes": "Methods = Online Methods, placed after Discussion. Results and Methods have topical subheadings; Discussion has none."
        },
        "analysis": {
          "body_words": 3500,
          "abstract_words": 150,
          "figures": 6,
          "extended_data": 10,
          "references": 50
        },
        "correspondence": {
          "words_range": "300-800",
          "figures": 1,
          "references": 10
        }
      },
      "style": "numbered",
      "scope": "Chemical engineering: process design, reaction engineering, separations, transport phenomena, scale-up"
    },

    "nature_machine_intelligence": {
      "name": "Nature Machine Intelligence",
      "url": "https://www.nature.com/natmachintell/for-authors",
      "impact_factor": 23.9,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3500,
          "body_excludes": "abstract, Methods, references, figure legends",
          "abstract_words": 150,
          "abstract_unreferenced": true,
          "figures": 6,
          "references": 50,
          "methods_location": "after_discussion",
          "structure": ["Introduction (no heading)", "Results", "Discussion", "Methods"],
          "notes": "Results and Methods have topical subheadings; Discussion does not. Also accepts Analysis format (100-150 word abstract)."
        },
        "analysis": {
          "body_words": 3500,
          "abstract_words_range": "100-150",
          "figures": 6,
          "references": 50
        },
        "correspondence": {
          "words_range": "500-1000",
          "figures": 1,
          "references": 10
        },
        "review": {
          "body_words_range": "3000-4000",
          "references": 100,
          "notes": "≤10% of refs should have short annotations explaining key contributions"
        }
      },
      "style": "numbered",
      "scope": "Machine learning, deep learning, robotics, AI, computer vision, NLP, reinforcement learning"
    },

    "nature_synthesis": {
      "name": "Nature Synthesis",
      "url": "https://www.nature.com/natsynth/for-authors",
      "impact_factor": 20.0,
      "acceptance_rate": "~8%",
      "article_types": {
        "article": {
          "body_words": 3000,
          "body_excludes": "abstract, methods, references, figure/table captions",
          "abstract_words": 150,
          "abstract_unreferenced": true,
          "figures": 6,
          "references": 50,
          "methods_location": "after_discussion",
          "methods_words": 3000,
          "structure": ["Introduction (no heading)", "Results", "Discussion", "Conclusions (optional)", "Methods"],
          "special_rules": [
            "NO schemes — use figures instead (unique to Nature Synthesis)",
            "Methods section CANNOT contain figures or tables — use Extended Data or SI",
            "Results and Discussion can be combined as 'Results and Discussion' with subheadings",
            "Discussion should be succinct with NO subheadings",
            "Non-graphical equations permitted in Methods"
          ],
          "notes": "Only one article type (Article). Spans short communications to full studies."
        },
        "correspondence": {
          "words_range": "300-800",
          "figures": 1,
          "references": 10
        }
      },
      "style": "numbered",
      "scope": "Synthetic chemistry and materials synthesis: total synthesis, synthetic methodology, automated synthesis, retrosynthetic analysis"
    }
  }
}


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