paper-citation-tracer · git:20260922.808ea2e · 2026-09-22 · sha256 8b436aa0500da115

paper-citation-tracer git:20260922.808ea2eA

Immutable. This exact content is served forever at /api/v1/blob/8b436aa0500da115.

---
name: paper-citation-tracer
description: |
  为学术写作与文献综述任务,定位 PDF 全文中与用户引文(中文或英文段落)对应的原文段落,
  用 PyMuPDF 添加高亮注释(黄色),输出标注后的 PDF 文件——用于核验引文出处是否属实。
  适用于:用户在论文/综述写作中提供"引文原文"和"参考文献 PDF 路径",要求把 PDF 中
  对应的定义段、关键术语、引用位置都标黄,方便直接对照原文核验引用。
  触发词:核验引用、引文溯源、高亮文献、高亮 PDF、文献段落标注、标黄引文、PDF 标注、
  verify citation、trace quote to source、highlight citations in PDF、mark up PDF with quotes。
---

# Paper Citation Tracer

为用户的引文(中文或英文段落)在指定的 PDF 全文中定位原文出处,并添加高亮注释——
把引文的原始证据翻出来,核验判断由人来做。

> 本仓库同时提供独立 CLI `highlight_citations.py`:没有 agent 环境时,
> 也可以直接在命令行完成同样的标注(见 README)。

## 适用场景

- **核验 AI 插入的引用**:AI 辅助写作给出的引用「看着像真的」,把带引用的正文段落 + 被引文献 PDF 交给 agent,标出原文出处,人工判断引用是否合理、有无过度转述
- 用户给出一段引文(中文或英文)+ 一组 PDF 路径,要求把 PDF 中真正对应引文定义的原文段标黄
- 输出:保留原 PDF 所有内容,仅在原文位置加 highlight 注释(不修改文字、不删页)

## 工作流

### 1. 通读 PDF,定位关键页

```python
import fitz  # PyMuPDF, pip install pymupdf
doc = fitz.open(pdf_path)
for pno in range(len(doc)):
    text = doc[pno].get_text()
    # 用引文的核心术语作为搜索关键词
    # 例如"holistic in nature"、"multidimensional construct"
    if "holistic in nature" in text.lower():
        print(f"Page {pno+1} matched")
        print(text[:1500])
```

**关键**:
- 优先看 abstract(第 1 页)和 introduction(前 3-4 页)
- 用 `len(doc)` 确认总页数,只在前 30% 找定义段,避免误中高频词
- 排除参考文献区:参考文献通常在第 N-3 页之后(看是否有 "References" 标题 + 大量作者-年份格式)

### 2. 提取精确短片段作为高亮关键词

**避免问题**:
- ❌ 长句(>50 字符)容易因换行/智能引号匹配失败
- ❌ 智能引号 `\u2019` 需归一化为 ASCII `'` 才能用 `search_for`
- ❌ 通用词(如"servicescape")全文出现 30+ 次,要用 `max_n=2~3` 限制

**经验**:
- ✅ 用 8-25 字符的独特短语(动词+宾语+限定词)
- ✅ 不含标点/数字/年份(容易在版式里被打断)
- ✅ 同一概念用 2-3 个不同片段作冗余匹配

```python
# 关键短片段示例
key_quotes = [
    "holistic in nature and involves",
    "cognitive, affective, emotional, social and physical",
    "Verhoef et al. (2009, p. 32) explicitly",
]
```

### 3. 添加高亮注释

```python
import fitz
import os

def add_hl(page, needle, color=(1.0, 0.9, 0.3), opacity=1.0, max_n=3):
    n = 0
    for r in page.search_for(needle)[:max_n]:
        annot = page.add_highlight_annot(r)
        annot.set_colors(stroke=color)
        annot.set_opacity(opacity)
        annot.update()
        n += 1
    return n

doc = fitz.open(pdf_path)
n = 0
for pno in range(min(5, len(doc))):  # 限定页码
    page = doc[pno]
    for kw in key_quotes:
        n += add_hl(page, kw, color=(1.0, 0.9, 0.3), max_n=2)
doc.save(out_path, garbage=4, deflate=True, clean=True)
doc.close()
```

### 4. 验证高亮覆盖

```python
# 必须验证:抽几个高亮 rect 出来,确认文本确实是引文对应的原文
doc = fitz.open(out_path)
for pno in range(len(doc)):
    page = doc[pno]
    for a in list(page.annots())[:5]:
        t = page.get_text("text", clip=a.rect).strip()[:100]
        if t:
            print(f"  | {t}")
doc.close()
```

如果验证发现关键词"命中"但匹配到错误位置(如同名术语在别处出现),缩小关键词或限定 max_n。

## 关键技术点

### 智能引号与换行

PyMuPDF 的 `page.search_for(needle)` 是字符级搜索,对以下情况不鲁棒:
- 智能引号 `\u2019` (curly apostrophe) vs ASCII `'`
- 连字 `fi` (\ufb01) vs "fi"
- 跨行换行:PDF 提取的文本中 `holistic in nature and` 可能被分成
  `holistic in\nnature and`,导致 search_for 失败

**解法**:
- 关键词只用 ASCII 字符,不带撇号
- 关键词不超过一行长度(避免跨换行)
- 对极特殊情况可用 `get_text("words")` 拼接 rect,再对每个 word 单独匹配

### 高亮颜色

- 黄色 `(1.0, 0.9, 0.3)` 是最常用的"荧光笔"色
- 设置 `set_opacity(1.0)` 表示完全不透明
- 如果 PDF 有彩色插图,可用半透明(0.5)让原图透出

### 排除参考文献

```python
text = page.get_text()
# 参考文献页通常以 "References" 标题开头 + 大量 "Author (Year)" 格式
if 'References' in text[:200] and 'Aaker' in text:  # 改用具体作者名
    continue
```

或更严格:只看页面前 30% 的页码(参考文献通常在最后 1/3)。

### 输出路径规范

- 输出文件加 `_highlighted` 后缀:`xxx_highlighted.pdf`
- 输出到当前工作目录或用户指定的位置
- 不要覆盖原 PDF

## 已知陷阱

1. **scan OCR 文档**:扫描版书刊的 PDF 提取文本常有空格错位、单字成行。
   关键词要短(5-10 字符)且不依赖完整短语。

2. **max_n 失控**:通用词如"servicescape"、"customer experience"全文 30+ 处,
   一定要用 `max_n=2~3` 限制每关键词匹配次数,避免整页变黄。

3. **MuPDF xref warning**:某些老 PDF 的 xref 表损坏,添加注释时可能报
   `MuPDF error: format error: cannot find object in xref`——这是非致命警告,
   文件仍能保存,PDF 阅读器可正常打开。

4. **第一篇常对不上**:用户给的引文对应的"经典出处"和实际 PDF 不一致的情况
   经常发生(如 Shostack 1984 服务蓝图 vs Surprenant 1987 服务接触)。发现第一篇
   对应不上时,立即告诉用户并提供正确引用建议。

5. **注释"not bound to any page"**:`doc[pno].add_highlight_annot(rect)` 把注释
   绑在临时 Page 对象上,下一次循环重建 `doc[pno]` 时临时对象被回收,注释随之
   失效——保存的 PDF 里一处高亮都没有,或循环中途报
   `FzErrorArgument: annotation not bound to any page`。
   **解法**:先 `page = doc[pno]` 存入变量,在同一次迭代里完成 add + set_colors +
   update。本仓库 CLI 已按此写法实现。

## 与其他 skill 的关系

- 不需要联网,纯本地 PDF 处理
- 与 `paper-finder` 互补:finder 找 PDF,本 skill 标注 PDF
- 与 `citation-manager` 互补:manager 写参考文献,本 skill 标引文出处