📘 项目背景

每次备课或者复习考试,常见的资料形式都是PDF 版课件:内容多、公式密集、还有各种选择题。
如果想把这些内容整理成可搜索、可编辑的 Markdown 笔记,传统方式通常是:

  • 手动复制文字(遇到公式就崩溃)
  • 自己用 LaTeX 写公式
  • 选择题一个个重排格式

不仅枯燥,而且非常浪费时间。

于是,我基于多模态大模型(支持看图)做了一个“课件 PDF → Markdown 笔记”自动转换工具
只需要选择输入/输出文件夹,就能把整批 PDF 课件自动转成结构化的 Markdown 文件,公式直接是 KaTeX 格式,选择题排版也自动搞定。


✨ 核心能力概览

功能 说明
📄 PDF 批量处理 支持选择一个文件夹,一次性处理多个 PDF 课件
🧠 AI OCR + 理解 用多模态大模型识别每一页内容,而不是简单图像 OCR
🧮 KaTeX 数学公式 生成符合 Markdown + KaTeX 语法的数学公式
📝 选择题结构化 自动识别“题干 + 选项”,用 A./B./C./D. 规范输出
⚡ 并发加速 利用 asyncio + aiohttp 并发调用 API,加快大课件处理速度
🔁 稳健重试机制 针对限流、内容安全、图片过大等情况做了自动重试与降级提示
🖼 智能图片压缩 图片太大时自动压缩/缩放,以适配 API 限制

🏗 整体流程与架构

用一个简单的流程图来概括整个系统:

1
2
3
4
5
6
7
8
9
10
11
┌────────────┐     ┌────────────────────┐     ┌────────────────────┐
│ 选择输入/输出 │ → │ PDF 转图片 (每页一图) │ → │ 异步并发调用 AI 模型 │
└────────────┘ └────────────────────┘ │ • OCR 识别 │
│ • KaTeX/Markdown 格式│
└─────────┬───────────┘

┌─────▼──────┐
│ 按页写入 .md│
│ • ## 第 N 页│
│ • 内容正文 │
└─────────────┘

核心模块对应到代码大致可以分成三部分:

  • PDF → 图片pdf_to_images
  • 单页图片 → Markdown 文本(AI 调用)process_image
  • 整本 PDF 组织与输出process_pdf / process_files / main

下面重点讲 AI 相关的部分。


🤖 AI 应用详解:从图片到 Markdown 笔记

1. 多模态模型:从“看图”直接输出 Markdown + KaTeX

传统 OCR 工具只能把图片中的文字“抠出来”,但:

  • 无法理解题目结构
  • 公式识别不友好,很难直接用于 Markdown/KaTeX
  • 选择题格式往往乱掉

在这个项目里,我直接把每一页课件渲染成图片,交给支持图像理解的多模态大模型处理。
在代码中,核心是一个精心设计的 system prompt,告诉模型我要的“输出格式”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
system_prompt = """
你是一个专业的 OCR + 数学排版助手,请把图片内容转成 Markdown + KaTeX 格式。

要求:
1. 识别图片上的所有文字和数学公式
2. 数学公式使用 $...$(行内)或 $$...$$(块级)包裹
3. 尽量用 KaTeX 支持的语法,例如:
- 分数:\\frac{a}{b}
- 开根号:\\sqrt{x}
- 上下标:x^2, x_n
4. 对于选择题:
- 题干用正常段落表示
- 选项使用 A. B. C. D. 的形式,每个选项单独一行
5. 保持原有的题号、结构层级(如“### 35. ...”)
"""

在真正的实现里,我还加了一些具体例子,让模型模仿我想要的格式,例如:

  • 如何写“第 35 题”的标题
  • 选择题 A./B./C./D. 的排版样式
  • 多行公式的写法

这样模型输出的结果基本可以直接贴到 Obsidian / Hexo / Markdown 编辑器里使用

注意:实际代码中我有更多细节提示,这里做了部分简化,避免暴露完整提示工程。


2. 异步并发调用:整本课件同时跑

一本课件往往几十页,如果一页一页串行调用 AI 接口,会非常慢。
所以我使用了 asyncio + aiohttp + Semaphore 进行并发控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CONCURRENCY = 20  # 同时最多处理 20 页

async def process_pdf(pdf_file, output_dir):
images = pdf_to_images(pdf_file) # [(page_number, img_bytes), ...]

async with aiohttp.ClientSession() as session:
semaphore = asyncio.Semaphore(CONCURRENCY)

tasks = [
process_image(session, image_data, semaphore, page_number)
for page_number, image_data in images
]

results = await asyncio.gather(*tasks)
# 按页顺序写入 Markdown 文件

process_image 里核心逻辑是:

  1. 进入信号量,保证最多只有 CONCURRENCY 个并发请求
  2. 把图片编码成 base64,按照兼容 OpenAI 的格式组织请求体
  3. 接收模型返回的 Markdown 文本
  4. 出错时根据错误类型做不同处理(重试、压缩、跳过等)

请求部分的结构大致如下(已去除具体地址与 Key):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
API_BASE_URL = "你的接口地址"  # 实际代码中为私有配置
API_KEY = "你的_API_Key"
MODEL_NAME = "你的多模态模型名称"

async def process_image(session, image_data, semaphore, page_number):
async with semaphore:
encoded_image = base64.b64encode(image_data).decode("utf-8")

response = await session.post(
f"{API_BASE_URL}/v1/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": MODEL_NAME,
"stream": False,
"messages": [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": "请按约定格式输出 Markdown 内容。"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{encoded_image}"
},
},
],
},
],
},
)
# 后续解析 response.json(),提取 content

这里刻意没有给出真实 API_BASE_URLAPI_KEY 和完整参数,只展示结构,方便你替换成自己的服务。


3. 容错策略:图片太大、内容敏感、限流… 都有对策

在实际使用中,AI 接口最容易出现几类问题:

  • 图片太大,超过模型限制
  • 内容被误判为不安全(例如包含“政治”“敏感”等关键词)
  • API 限流或网络不稳定
  • 某些页结构特殊,模型多次失败

我在 process_image 中对这些情况做了分类处理

3.1 文件过大:自动压缩再重试

当错误信息中出现“exceeded limit on max bytes”“data-uri item”等关键字时,判断为图片太大

  • 第一次遇到时,会调用 compress_image
    • 先尝试降低 JPEG 质量
    • 还不够再缩小分辨率
    • 最后保底用一个较小尺寸 + 低质量版本
  • 再用压缩后的图片重新调用 AI
  • 如果多次压缩仍不满足大小限制,就在结果中写入一段提示文本,告诉你哪一页没能处理

压缩逻辑大致如下(去掉了一些打印细节):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from PIL import Image
import io

def compress_image(img_bytes, max_size_bytes, quality=85):
img = Image.open(io.BytesIO(img_bytes))

if img.mode in ("RGBA", "LA", "P"):
img = img.convert("RGB")

# 先尝试不同质量
for q in range(quality, 10, -10):
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=q, optimize=True)
data = buf.getvalue()
if len(data) <= max_size_bytes:
return data

# 再尝试按比例缩小尺寸
for scale in [0.8, 0.6, 0.4, 0.2]:
new_size = (int(img.width * scale), int(img.height * scale))
resized = img.resize(new_size, Image.Resampling.LANCZOS)
buf = io.BytesIO()
resized.save(buf, format="JPEG", quality=60, optimize=True)
data = buf.getvalue()
if len(data) <= max_size_bytes:
return data

# 仍然太大则返回一个更激进的降级版本
# ...

3.2 内容安全/敏感词:跳过该页并写入说明

当错误信息里包含“data_inspection_failed”“inappropriate content”“政治”“敏感”“policy”等关键词时:

  • 在命令行输出详细提示
  • 在生成的 Markdown 中写入一段带 ⚠️ 提示的说明
  • 跳过该页内容的进一步重试

这样你在之后复盘时,可以清晰看到:

第 X 页可能包含敏感内容/质量过差,已跳过处理。

3.3 限流与网络问题:指数退避重试

对于限流、配额、网络超时这类错误:

  • 统一采用最多 N 次重试 + 指数退避策略:
    • 第 1 次失败后等 0 秒
    • 第 2 次失败等 2 秒
    • 第 3 次失败等 4 秒
  • 超过最大重试次数依然失败,则在该页输出 None,并在最终统计中归为“失败页面”。

📄 从 PDF 到 Markdown 文件:输出结构

每个 PDF 最终会对应一个同名的 .md 文件,例如:

  • 概率论-期中复习.pdf概率论-期中复习.md

文件内容按“每一页一个小节”组织:

1
2
3
4
5
6
7
## 第 1 页

(这里是 AI 生成的 Markdown + KaTeX 内容)

## 第 2 页

(这里是第 2 页的内容)

脚本会统计:

  • 成功处理了多少页
  • 哪些页失败
  • 失败页的编号列表

方便你后续人工检查少数问题页,而不是整本都自己重敲。


🧪 使用方式(简略版)

  1. 准备环境

    • 安装依赖(示例):

      1
      pip install aiohttp pymupdf pillow
    • 在代码中配置好自己的:

      • API_BASE_URL
      • API_KEY
      • MODEL_NAME
  2. 运行脚本

    • 直接执行脚本:

      1
      python 课件PDF转Markdown.py
    • 弹出 Tk 界面:

      • 先选择包含 PDF 的输入文件夹
      • 再选择输出 Markdown 的目标文件夹
  3. 查看结果

    • 每个 PDF 会对应一个 .md 文件
    • 可直接导入 Obsidian / Hexo / 任意 Markdown 编辑器

出于安全考虑,文中没有展示任何真实 Key 或完整配置,实际使用时请将敏感信息放到环境变量或独立配置文件中。


🔐 关于 API Key 与隐私安全

在实际项目中,有几点安全建议:

  • 不要把 API Key 写死在代码仓库里
    • 推荐使用环境变量:os.getenv("AI_API_KEY")
    • 或使用 .env / 独立配置文件(不提交到 Git)
  • 不要在博客/截图中展示完整请求地址和 Key
  • 如果是批量处理学术课件:
    • 请确认内容不涉及隐私/敏感数据
    • 如有必要,可在本地或内网环境部署自托管模型

🎯 总结:让 AI 帮你做“搬运工”,自己做“思考者”

这个小工具本质上做了三件事:

  • 视觉问题(PDF 课件)转化为文本问题(Markdown 笔记)
  • 机械重复的整理工作交给 AI
  • 把你的时间从格式处理中解放出来,留给真正的理解与思考

对于学生、老师、内容创作者来说,这是一种非常实用的 AI 应用形态:
不是炫酷的 Demo,而是每天都能用、用完就明显省时间的“小工具”。

如果你也有大量 PDF 课件、题库、讲义想要结构化,不妨试着把这个思路迁移到自己的场景里,让 AI 帮你打基础,你只负责在优质的 Markdown 笔记上“再加工”。