GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

软件研发 --- Dify 生成 PPT 方案分析

 

您提供了一套极其详尽的纯离线AI PPT生成解决方案文档,涵盖了从底层原理、组件选型、代码实现到界面交互的全流程。这份资料本身已经是一份高质量的内部实施指南,足以支撑一个完整项目的落地。

鉴于您没有直接提问,而是展示了一整套备选方案(自研RAG流水线 vs. Dify+第三方API vs. Presenton),我判断您的核心诉求是:在这些方案中做出最优选型,并确认技术细节的可行性。

以下是针对您所提供内容的整合评估、方案对比与最终落地建议。

一、核心方案对比矩阵

您实际上提供了三条技术路径,我将其提炼为下表:

 
对比维度方案A:自研全栈流水线 (您提供的 python-pptx 代码)方案B:Dify 编排 + 外部渲染方案C:Presenton 一体化工具
离线/隐私性 ⭐⭐⭐⭐⭐ (完全离线,数据不外传) ⭐⭐⭐⭐ (Dify 离线,若接 API 则部分在线) ⭐⭐⭐⭐⭐ (Docker 镜像内全离线)
排版美观度 ⭐⭐⭐ (依赖 Python 代码调优,上限高但工作量大) ⭐⭐⭐⭐ (依赖 Gamma/MindShow 等专业渲染) ⭐⭐⭐⭐ (有专业模板,支持风格学习)
知识库能力 ⭐⭐⭐⭐⭐ (ChromaDB/BGE-M3,自由度极高) ⭐⭐⭐⭐ (Dify 自带 RAG 引擎,方便) ⭐⭐⭐ (支持文档上传,但不如专业 RAG 灵活)
开发运维成本 ⭐⭐ (需维护代码、环境、模型服务) ⭐⭐⭐⭐ (Dify 低代码,但渲染服务需额外配置) ⭐⭐⭐⭐⭐ (Docker 一条命令启动)
定制化深度 ⭐⭐⭐⭐⭐ (代码级控制,想改什么改什么) ⭐⭐⭐ (受限于工具 API 和 Dify 插件) ⭐⭐⭐ (受限于 Presenton 支持的功能)
硬件门槛 较高 (建议 16G 显存跑 14B 模型) 中等 (Dify 轻量,渲染走 API 不占算力) 较高 (依赖本地模型推理)

二、场景化选型结论

根据您的实际使用场景,我给出以下明确的选型建议:

1. 如果你是:个人开发者/极客,追求技术掌控与完全隐私

👉 推荐方案:方案A(您提供的自研全栈流水线)

理由:

  • 您的文档已经提供了 knowledge_base.pyppt_renderer.py 等核心代码,这不是伪代码,是直接可运行的工程框架。

  • 优势:您可以完全控制排版逻辑,将来要增加“甘特图生成”、“思维导图插入”等功能,只需要修改 Python 代码。

  • 优化建议:针对您代码中的 PPTRenderer,建议将 Marp 作为备选渲染后端。Marp 通过 Markdown 转 PPT,排版比纯 python-pptx 调坐标更优雅。

2. 如果你是:企业 IT/运维,需要给业务部门提供一个内部工具

👉 推荐方案:方案C(Presenton)+ 方案A 的知识库模块

理由:

  • 业务部门无法接受“通过写 Python 命令行生成 PPT”。他们需要的是一个 Web 页面,输入主题,上传文档,下载 PPT。

  • Presenton 提供了现成的 UI 界面和模板管理,部署成本极低(docker run)。

  • 知识库增强:如果 Presenton 自带的文档上传不够用,可以复用您方案A中的 KnowledgeBase 模块,单独起一个 FastAPI 服务,让 Presenton 通过 Custom Tool 调用来获取更精准的上下文。

3. 如果你追求:PPT 视觉效果达到“咨询公司/发布会”级别

👉 推荐方案:混合架构(Dify + Gamma / 美图AI PPT)

必须承认的现实:

  • 用代码画 PPT(python-pptx)能保证内容正确,但绝对做不出设计感很强的模板。

  • Gamma.app 或 美图AI PPT 的渲染引擎是专业设计师调试过的。

  • 折中方案:用您的离线知识库 + Ollama 生成 内容骨架(JSON),然后通过手动上传的方式导入到 Gamma 或 MindShow 中进行一键美化。

    • 这虽然不是“全自动离线”,但在排版美观和内容准确之间取得了最佳平衡。

三、针对您代码方案的深度优化建议

如果您决定采用方案A(自研流水线),以下是对您代码的几点关键补充,能显著提升生成质量:

1. 引入 Marp 作为排版引擎(强烈推荐)

您的 PPTRenderer 虽然强大,但维护 Inches(0.5) 这种硬编码极其痛苦。
建议架构调整:

python
# 让 LLM 输出 Marp 格式的 Markdown
prompt = """
请生成 Marp 格式的 PPT,每页用 --- 分割。
---
<!-- _class: lead -->
# 主标题
## 副标题
---
<!-- _class: body -->
## 核心观点
- 要点1
- 要点2
"""

# 保存为 temp.md,调用系统命令
import subprocess
subprocess.run(["marp", "temp.md", "--pptx", "-o", "output.pptx"])

优点:排版代码量减少 90%,支持 CSS 主题自定义。

2. 显存不足时的保底策略

您文档中提到 Qwen2.5-14B 需要 10G 显存。如果机器配置较低(如只有 8G 显存),建议使用 Ollama 的 num_ctx 限制或换用 Qwen2.5-7B-Int4 量化版,对于 PPT 大纲生成来说,7B 模型的逻辑能力足够,且速度更快。

3. 内容去“AI 味”

在 ppt_generator.py 的 Prompt 中增加一条强制性规则,能极大提升专业度:

text
- 禁止使用任何过渡句,例如:“接下来我们将介绍...”、“综上所述...”。
- 所有要点必须以**动宾结构**开头(例如:“建立应急响应机制”、“缩短故障恢复时间”)。
- 每页核心观点用一句不超过 15 字的黑体字展示在顶部。

四、总结

您已经掌握了离线 AI PPT 生成的最高技术栈。

  • 代码层面:您的 main_pipeline.py 是可直接投产的骨架。

  • 决策层面:如果不希望陷入前端和排版的细节泥潭,Presenton 是当前离线开源生态中的最优解;如果追求极致定制,您的自研方案是唯一的路。

需要我针对某个具体模块(比如如何用 LangGraph 做更复杂的多页 PPT 自动拆解,或者如何优化 ChromaDB 的检索精度)进行深入展开吗?

 
 
 
claude opus

 

# 纯离线 AI LLM + 知识库生成高质量 PPT 的最佳方案

## 整体架构

```
┌─────────────────────────────────────────────────────┐
│ 用户交互层 │
│ (Web UI / CLI / Desktop) │
├─────────────────────────────────────────────────────┤
│ 编排调度层 │
│ (LangChain / LlamaIndex) │
├──────────┬──────────────┬───────────────────────────┤
│ 本地LLM │ RAG知识库 │ PPT生成引擎 │
│ (Ollama) │ (向量数据库) │ (python-pptx/模板引擎) │
├──────────┴──────────────┴───────────────────────────┤
│ 本地存储层 │
│ (文档/向量库/模板库/图片素材库) │
└─────────────────────────────────────────────────────┘
```

---

## 一、核心组件选型

### 1. 本地 LLM 推理引擎

| 方案 | 推荐模型 | 显存需求 | 特点 |
|------|---------|---------|------|
| **Ollama** (首推) | Qwen2.5-72B-Q4, DeepSeek-V2.5, Llama3.1-70B | 48GB+ | 部署最简单 |
| llama.cpp | 同上的GGUF格式 | 可CPU运行 | 灵活,支持CPU+GPU混合 |
| vLLM | 原始精度模型 | 80GB+ | 吞吐量最高 |
| LocalAI | 多种格式 | 灵活 | OpenAI API兼容 |

**推荐配置:**
```bash
# 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh

# 拉取推荐模型(中文能力强)
ollama pull qwen2.5:72b-instruct-q4_K_M # 高质量首选
ollama pull qwen2.5:32b-instruct-q4_K_M # 平衡之选
ollama pull deepseek-v2.5:236b-q4_K_M # 旗舰级(需多卡)

# 嵌入模型(用于知识库)
ollama pull bge-m3 # 中英文嵌入最佳
ollama pull nomic-embed-text # 轻量替代
```

### 2. 知识库 (RAG) 系统

```
文档 → 解析 → 分块 → 向量化 → 向量数据库 → 检索 → LLM生成
```

| 组件 | 推荐方案 | 说明 |
|------|---------|------|
| 文档解析 | **Unstructured** / MinerU / Docling | 支持PDF/Word/网页等 |
| 文本分块 | LangChain RecursiveCharacterTextSplitter | 语义分块 |
| 嵌入模型 | **BGE-M3** (本地) | 中英文双语最强 |
| 向量数据库 | **ChromaDB** / Milvus Lite / FAISS | 纯本地,无需服务 |
| RAG框架 | **LlamaIndex** / LangChain | 编排检索+生成 |

---

## 二、完整实现方案

### 项目结构

```
ppt-generator/
├── config/
│ └── settings.yaml # 配置文件
├── knowledge_base/
│ ├── documents/ # 原始文档
│ ├── vector_store/ # 向量数据库存储
│ └── kb_manager.py # 知识库管理
├── llm/
│ └── local_llm.py # LLM调用封装
├── ppt_engine/
│ ├── templates/ # PPT模板库
│ │ ├── business_blue.pptx
│ │ ├── tech_dark.pptx
│ │ └── minimal_white.pptx
│ ├── assets/ # 图标/图片素材
│ ├── content_generator.py # 内容生成
│ ├── layout_engine.py # 布局引擎
│ ├── chart_generator.py # 图表生成
│ └── ppt_builder.py # PPT组装
├── app.py # 主程序入口
├── web_ui.py # Gradio Web界面
└── requirements.txt
```

### 核心代码实现

#### `requirements.txt`
```txt
ollama
langchain>=0.2.0
langchain-community
llama-index>=0.10.0
chromadb>=0.4.0
python-pptx>=0.6.23
sentence-transformers
unstructured[all-docs]
pydantic>=2.0
gradio>=4.0
Pillow
matplotlib
plotly
kaleido
pyyaml
```

#### `config/settings.yaml`
```yaml
llm:
provider: "ollama"
model: "qwen2.5:32b-instruct-q4_K_M"
base_url: "http://localhost:11434"
temperature: 0.7
num_ctx: 8192

embedding:
provider: "ollama"
model: "bge-m3"
base_url: "http://localhost:11434"

knowledge_base:
chunk_size: 1000
chunk_overlap: 200
top_k: 8
vector_store_path: "./knowledge_base/vector_store"
documents_path: "./knowledge_base/documents"

ppt:
default_template: "business_blue"
templates_path: "./ppt_engine/templates"
output_path: "./output"
max_slides: 30
```

#### `llm/local_llm.py` — LLM 封装
```python
import json
import yaml
from ollama import Client
from typing import Optional

class LocalLLM:
def __init__(self, config_path: str = "config/settings.yaml"):
with open(config_path, 'r') as f:
config = yaml.safe_load(f)

self.config = config['llm']
self.client = Client(host=self.config['base_url'])
self.model = self.config['model']

def generate(self, prompt: str, system_prompt: str = "",
json_mode: bool = False, temperature: float = None) -> str:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})

options = {
"temperature": temperature or self.config['temperature'],
"num_ctx": self.config['num_ctx'],
}

response = self.client.chat(
model=self.model,
messages=messages,
format="json" if json_mode else "",
options=options
)

return response['message']['content']

def generate_structured(self, prompt: str, system_prompt: str = "") -> dict:
"""生成结构化JSON输出"""
result = self.generate(prompt, system_prompt, json_mode=True)
try:
return json.loads(result)
except json.JSONDecodeError:
# 尝试提取JSON部分
import re
json_match = re.search(r'\{[\s\S]*\}', result)
if json_match:
return json.loads(json_match.group())
raise ValueError(f"无法解析LLM输出为JSON: {result[:200]}")
```

#### `knowledge_base/kb_manager.py` — 知识库管理
```python
import os
import yaml
import chromadb
from chromadb.config import Settings
from langchain_community.document_loaders import (
DirectoryLoader, PyPDFLoader, Docx2txtLoader,
TextLoader, UnstructuredMarkdownLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
from typing import List, Dict
import hashlib

class KnowledgeBaseManager:
def __init__(self, config_path: str = "config/settings.yaml"):
with open(config_path, 'r') as f:
config = yaml.safe_load(f)

self.kb_config = config['knowledge_base']
self.embed_config = config['embedding']

# 初始化嵌入模型(本地)
self.embed_model = SentenceTransformer(
'BAAI/bge-m3', # 首次会下载,之后离线可用
cache_folder="./models/embeddings"
)

# 初始化ChromaDB(纯本地)
self.chroma_client = chromadb.PersistentClient(
path=self.kb_config['vector_store_path'],
settings=Settings(anonymized_telemetry=False)
)
self.collection = self.chroma_client.get_or_create_collection(
name="ppt_knowledge_base",
metadata={"hnsw:space": "cosine"}
)

# 文本分割器
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.kb_config['chunk_size'],
chunk_overlap=self.kb_config['chunk_overlap'],
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " "]
)

def ingest_documents(self, doc_path: str = None):
"""导入文档到知识库"""
doc_path = doc_path or self.kb_config['documents_path']

# 支持多种文档格式
loaders = {
"*.pdf": PyPDFLoader,
"*.docx": Docx2txtLoader,
"*.txt": TextLoader,
"*.md": UnstructuredMarkdownLoader,
}

all_docs = []
for glob_pattern, loader_cls in loaders.items():
try:
loader = DirectoryLoader(
doc_path, glob=glob_pattern,
loader_cls=loader_cls,
show_progress=True
)
docs = loader.load()
all_docs.extend(docs)
print(f" 加载 {glob_pattern}: {len(docs)} 个文件")
except Exception as e:
print(f" 加载 {glob_pattern} 出错: {e}")

# 分块
chunks = self.text_splitter.split_documents(all_docs)
print(f"总共 {len(chunks)} 个文本块")

# 批量嵌入并存储
batch_size = 64
for i in range(0, len(chunks), batch_size):
batch = chunks[i:i+batch_size]
texts = [chunk.page_content for chunk in batch]
metadatas = [chunk.metadata for chunk in batch]
ids = [hashlib.md5(t.encode()).hexdigest() for t in texts]

embeddings = self.embed_model.encode(
texts, normalize_embeddings=True
).tolist()

self.collection.upsert(
ids=ids,
embeddings=embeddings,
documents=texts,
metadatas=metadatas
)
print(f" 已索引 {min(i+batch_size, len(chunks))}/{len(chunks)}")

print(f"知识库构建完成,共 {self.collection.count()} 条记录")

def search(self, query: str, top_k: int = None) -> List[Dict]:
"""检索相关文档"""
top_k = top_k or self.kb_config['top_k']

query_embedding = self.embed_model.encode(
[query], normalize_embeddings=True
).tolist()

results = self.collection.query(
query_embeddings=query_embedding,
n_results=top_k,
include=["documents", "metadatas", "distances"]
)

retrieved = []
for doc, meta, dist in zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
):
retrieved.append({
"content": doc,
"metadata": meta,
"relevance_score": 1 - dist # cosine距离转相似度
})

return retrieved

def get_context(self, query: str, top_k: int = None) -> str:
"""获取格式化的上下文文本"""
results = self.search(query, top_k)
context_parts = []
for i, r in enumerate(results, 1):
source = r['metadata'].get('source', '未知来源')
context_parts.append(
f"[参考资料 {i}] (来源: {source}, 相关度: {r['relevance_score']:.2f})\n"
f"{r['content']}"
)
return "\n\n---\n\n".join(context_parts)
```

#### `ppt_engine/content_generator.py` — 核心内容生成(多步骤 Pipeline)

```python
import json
from typing import Dict, List, Optional
from llm.local_llm import LocalLLM
from knowledge_base.kb_manager import KnowledgeBaseManager

class PPTContentGenerator:
def __init__(self):
self.llm = LocalLLM()
self.kb = KnowledgeBaseManager()

def generate_outline(self, topic: str, requirements: str = "",
num_slides: int = 15) -> Dict:
"""第一步:生成PPT大纲"""

# RAG检索相关知识
context = self.kb.get_context(topic, top_k=10)

system_prompt = """你是一位专业的PPT策划专家,擅长制作高质量的商业演示文稿。
你需要根据用户的主题和参考资料,生成结构清晰、逻辑严密的PPT大纲。
请以JSON格式输出。"""

prompt = f"""请为以下主题生成一份PPT大纲:

## 主题
{topic}

## 额外要求
{requirements if requirements else "无特殊要求"}

## 参考资料
{context}

## 输出要求
生成约{num_slides}页幻灯片的大纲,JSON格式如下:
{{
"title": "PPT总标题",
"subtitle": "副标题",
"target_audience": "目标受众",
"sections": [
{{
"section_title": "章节标题",
"slides": [
{{
"slide_number": 1,
"title": "幻灯片标题",
"layout_type": "title_slide|content|two_column|chart|image_text|bullet_points|quote|comparison|timeline|summary",
"key_points": ["要点1", "要点2"],
"notes": "演讲者备注/说明"
}}
]
}}
]
}}"""

outline = self.llm.generate_structured(prompt, system_prompt)
return outline

def generate_slide_content(self, outline: Dict, slide_info: Dict,
section_context: str) -> Dict:
"""第二步:为每一页生成详细内容"""

# 针对当前slide的主题做RAG检索
slide_query = f"{slide_info['title']} {' '.join(slide_info.get('key_points', []))}"
context = self.kb.get_context(slide_query, top_k=5)

system_prompt = """你是PPT内容撰写专家。请根据大纲和参考资料,为指定的幻灯片生成详细内容。
内容要求:
1. 文字精炼,每个要点不超过2行
2. 使用具体数据和案例支撑观点
3. 逻辑清晰,层次分明
请以JSON格式输出。"""

layout_type = slide_info.get('layout_type', 'bullet_points')

# 根据不同布局类型定义输出格式
layout_formats = {
"title_slide": '''{{
"title": "主标题",
"subtitle": "副标题",
"author": "作者/团队",
"date": "日期"
}}''',
"bullet_points": '''{{
"title": "标题",
"bullets": [
{{"main": "主要点", "sub": "补充说明(可选)"}},
...
],
"footer_note": "底部备注(可选)"
}}''',
"two_column": '''{{
"title": "标题",
"left_column": {{
"heading": "左列标题",
"content": ["要点1", "要点2", ...]
}},
"right_column": {{
"heading": "右列标题",
"content": ["要点1", "要点2", ...]
}}
}}''',
"chart": '''{{
"title": "标题",
"chart_type": "bar|line|pie|radar",
"chart_data": {{
"categories": ["类别1", "类别2", ...],
"series": [
{{"name": "系列名", "values": [数值1, 数值2, ...]}}
]
}},
"insight": "图表核心洞察",
"source": "数据来源"
}}''',
"comparison": '''{{
"title": "标题",
"items": [
{{"name": "项目A", "features": ["特点1", "特点2", ...]}},
{{"name": "项目B", "features": ["特点1", "特点2", ...]}}
],
"conclusion": "对比结论"
}}''',
"timeline": '''{{
"title": "标题",
"events": [
{{"time": "时间点", "event": "事件描述", "detail": "详细说明"}},
...
]
}}''',
"quote": '''{{
"title": "标题",
"quote": "引用内容",
"author": "引用来源",
"commentary": "评论说明"
}}''',
"summary": '''{{
"title": "总结标题",
"key_takeaways": ["核心要点1", "核心要点2", ...],
"call_to_action": "行动号召",
"contact_info": "联系方式(可选)"
}}'''
}

format_template = layout_formats.get(layout_type, layout_formats["bullet_points"])

prompt = f"""## PPT总主题
{outline.get('title', '')}

## 当前章节
{section_context}

## 当前幻灯片信息
- 页码: {slide_info['slide_number']}
- 标题: {slide_info['title']}
- 布局类型: {layout_type}
- 关键要点: {json.dumps(slide_info.get('key_points', []), ensure_ascii=False)}

## 参考资料
{context}

## 输出格式
请按以下JSON格式输出该页的详细内容:
{format_template}

同时在JSON中添加 "speaker_notes": "演讲者备注" 字段。"""

content = self.llm.generate_structured(prompt, system_prompt)
content['layout_type'] = layout_type
return content

def generate_full_ppt_content(self, topic: str, requirements: str = "",
num_slides: int = 15) -> Dict:
"""完整的PPT内容生成Pipeline"""

print("📋 第1步:生成PPT大纲...")
outline = self.generate_outline(topic, requirements, num_slides)
print(f" ✅ 大纲完成:{outline.get('title', topic)}")
print(f" 📊 共 {sum(len(s['slides']) for s in outline['sections'])} 页幻灯片")

print("\n📝 第2步:生成每页详细内容...")
all_slides = []

for section in outline['sections']:
section_context = f"章节:{section['section_title']}"

for slide_info in section['slides']:
print(f" 🔄 生成第 {slide_info['slide_number']} 页: {slide_info['title']}")

slide_content = self.generate_slide_content(
outline, slide_info, section_context
)
slide_content['slide_number'] = slide_info['slide_number']
slide_content['section'] = section['section_title']
all_slides.append(slide_content)

print("\n✨ 第3步:内容优化和一致性检查...")
# 可选:让LLM审查整体一致性

result = {
"metadata": {
"title": outline.get('title', topic),
"subtitle": outline.get('subtitle', ''),
"target_audience": outline.get('target_audience', ''),
"total_slides": len(all_slides)
},
"outline": outline,
"slides": all_slides
}

return result
```

#### `ppt_engine/ppt_builder.py` — PPT 文件构建

```python
import os
import json
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.chart import XL_CHART_TYPE
from pptx.chart.data import CategoryChartData
from typing import Dict, List, Optional
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from io import BytesIO

class PPTBuilder:
"""高质量PPT构建引擎"""

# 配色方案
COLOR_SCHEMES = {
"business_blue": {
"primary": RGBColor(0x00, 0x52, 0x9B),
"secondary": RGBColor(0x00, 0x96, 0xD6),
"accent": RGBColor(0xFF, 0x8C, 0x00),
"text_dark": RGBColor(0x33, 0x33, 0x33),
"text_light": RGBColor(0xFF, 0xFF, 0xFF),
"bg_light": RGBColor(0xF5, 0xF7, 0xFA),
"bg_dark": RGBColor(0x00, 0x3D, 0x73),
},
"tech_dark": {
"primary": RGBColor(0x1A, 0x1A, 0x2E),
"secondary": RGBColor(0x16, 0x21, 0x3E),
"accent": RGBColor(0x0F, 0xCE, 0xDD),
"text_dark": RGBColor(0xE0, 0xE0, 0xE0),
"text_light": RGBColor(0xFF, 0xFF, 0xFF),
"bg_light": RGBColor(0x1A, 0x1A, 0x2E),
"bg_dark": RGBColor(0x0F, 0x0F, 0x1A),
},
"minimal_white": {
"primary": RGBColor(0x2D, 0x2D, 0x2D),
"secondary": RGBColor(0x75, 0x75, 0x75),
"accent": RGBColor(0xE8, 0x4D, 0x39),
"text_dark": RGBColor(0x2D, 0x2D, 0x2D),
"text_light": RGBColor(0xFF, 0xFF, 0xFF),
"bg_light": RGBColor(0xFF, 0xFF, 0xFF),
"bg_dark": RGBColor(0x2D, 0x2D, 0x2D),
}
}

def __init__(self, template_path: str = None, color_scheme: str = "business_blue"):
if template_path and os.path.exists(template_path):
self.prs = Presentation(template_path)
else:
self.prs = Presentation()
# 设置16:9比例
self.prs.slide_width = Inches(13.333)
self.prs.slide_height = Inches(7.5)

self.colors = self.COLOR_SCHEMES.get(color_scheme, self.COLOR_SCHEMES["business_blue"])
self.slide_width = self.prs.slide_width
self.slide_height = self.prs.slide_height

def _add_background(self, slide, color: RGBColor = None):
"""设置幻灯片背景"""
background = slide.background
fill = background.fill
fill.solid()
fill.fore_color.rgb = color or self.colors['bg_light']

def _add_text_box(self, slide, left, top, width, height,
text: str, font_size: int = 18,
color: RGBColor = None, bold: bool = False,
alignment: PP_ALIGN = PP_ALIGN.LEFT,
font_name: str = "微软雅黑"):
"""添加文本框"""
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True

p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.color.rgb = color or self.colors['text_dark']
p.font.bold = bold
p.font.name = font_name
p.alignment = alignment

return txBox

def _add_shape_with_text(self, slide, shape_type, left, top, width, height,
text: str, fill_color: RGBColor = None,
font_size: int = 14, font_color: RGBColor = None):
"""添加带文字的形状"""
from pptx.enum.shapes import MSO_SHAPE
shape = slide.shapes.add_shape(shape_type, left, top, width, height)
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color or self.colors['primary']
shape.line.fill.background()

tf = shape.text_frame
tf.word_wrap = True
tf.paragraphs[0].text = text
tf.paragraphs[0].font.size = Pt(font_size)
tf.paragraphs[0].font.color.rgb = font_color or self.colors['text_light']
tf.paragraphs[0].font.name = "微软雅黑"
tf.paragraphs[0].alignment = PP_ALIGN.CENTER

return shape

def build_title_slide(self, slide_data: Dict):
"""构建标题页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6]) # 空白布局
self._add_background(slide, self.colors['bg_dark'])

# 装饰线条
from pptx.enum.shapes import MSO_SHAPE
line = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(1), Inches(3.2), Inches(2), Inches(0.05)
)
line.fill.solid()
line.fill.fore_color.rgb = self.colors['accent']
line.line.fill.background()

# 主标题
self._add_text_box(
slide, Inches(1), Inches(1.5), Inches(11), Inches(1.5),
slide_data.get('title', ''), font_size=44,
color=self.colors['text_light'], bold=True,
alignment=PP_ALIGN.LEFT
)

# 副标题
self._add_text_box(
slide, Inches(1), Inches(3.5), Inches(8), Inches(1),
slide_data.get('subtitle', ''), font_size=22,
color=self.colors['secondary'],
alignment=PP_ALIGN.LEFT
)

# 作者/日期
author = slide_data.get('author', '')
date = slide_data.get('date', '')
if author or date:
self._add_text_box(
slide, Inches(1), Inches(5.5), Inches(6), Inches(0.5),
f"{author} | {date}", font_size=14,
color=self.colors['text_light'],
alignment=PP_ALIGN.LEFT
)

def build_bullet_slide(self, slide_data: Dict):
"""构建要点列表页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
self._add_background(slide)

# 标题区域背景条
from pptx.enum.shapes import MSO_SHAPE
header_bg = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(0), Inches(0), self.slide_width, Inches(1.2)
)
header_bg.fill.solid()
header_bg.fill.fore_color.rgb = self.colors['primary']
header_bg.line.fill.background()

# 标题
self._add_text_box(
slide, Inches(0.8), Inches(0.2), Inches(11), Inches(0.8),
slide_data.get('title', ''), font_size=32,
color=self.colors['text_light'], bold=True
)

# 要点内容
bullets = slide_data.get('bullets', [])
y_start = Inches(1.8)

for i, bullet in enumerate(bullets):
if isinstance(bullet, dict):
main_text = bullet.get('main', '')
sub_text = bullet.get('sub', '')
else:
main_text = str(bullet)
sub_text = ''

# 序号圆圈
circle = slide.shapes.add_shape(
MSO_SHAPE.OVAL,
Inches(0.8), y_start + Inches(i * 1.0) + Inches(0.05),
Inches(0.4), Inches(0.4)
)
circle.fill.solid()
circle.fill.fore_color.rgb = self.colors['accent']
circle.line.fill.background()
tf = circle.text_frame
tf.paragraphs[0].text = str(i + 1)
tf.paragraphs[0].font.size = Pt(14)
tf.paragraphs[0].font.color.rgb = self.colors['text_light']
tf.paragraphs[0].font.bold = True
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
tf.paragraphs[0].font.name = "微软雅黑"

# 主要点文本
self._add_text_box(
slide, Inches(1.5), y_start + Inches(i * 1.0),
Inches(10), Inches(0.5),
main_text, font_size=20, bold=True,
color=self.colors['text_dark']
)

# 补充说明
if sub_text:
self._add_text_box(
slide, Inches(1.5), y_start + Inches(i * 1.0) + Inches(0.45),
Inches(10), Inches(0.4),
sub_text, font_size=14,
color=self.colors['secondary']
)

def build_two_column_slide(self, slide_data: Dict):
"""构建双栏页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
self._add_background(slide)

from pptx.enum.shapes import MSO_SHAPE

# 标题
header_bg = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(0), Inches(0), self.slide_width, Inches(1.2)
)
header_bg.fill.solid()
header_bg.fill.fore_color.rgb = self.colors['primary']
header_bg.line.fill.background()

self._add_text_box(
slide, Inches(0.8), Inches(0.2), Inches(11), Inches(0.8),
slide_data.get('title', ''), font_size=32,
color=self.colors['text_light'], bold=True
)

# 左栏
left_col = slide_data.get('left_column', {})
left_box = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(0.5), Inches(1.6), Inches(5.8), Inches(5.2)
)
left_box.fill.solid()
left_box.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
left_box.line.color.rgb = self.colors['primary']
left_box.line.width = Pt(2)

self._add_text_box(
slide, Inches(0.8), Inches(1.8), Inches(5.2), Inches(0.6),
left_col.get('heading', ''), font_size=22, bold=True,
color=self.colors['primary']
)

left_content = left_col.get('content', [])
for j, item in enumerate(left_content):
self._add_text_box(
slide, Inches(1.0), Inches(2.6 + j * 0.6), Inches(5), Inches(0.5),
f"• {item}", font_size=16, color=self.colors['text_dark']
)

# 右栏
right_col = slide_data.get('right_column', {})
right_box = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(6.8), Inches(1.6), Inches(5.8), Inches(5.2)
)
right_box.fill.solid()
right_box.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
right_box.line.color.rgb = self.colors['secondary']
right_box.line.width = Pt(2)

self._add_text_box(
slide, Inches(7.1), Inches(1.8), Inches(5.2), Inches(0.6),
right_col.get('heading', ''), font_size=22, bold=True,
color=self.colors['secondary']
)

right_content = right_col.get('content', [])
for j, item in enumerate(right_content):
self._add_text_box(
slide, Inches(7.3), Inches(2.6 + j * 0.6), Inches(5), Inches(0.5),
f"• {item}", font_size=16, color=self.colors['text_dark']
)

def build_chart_slide(self, slide_data: Dict):
"""构建图表页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
self._add_background(slide)

from pptx.enum.shapes import MSO_SHAPE

# 标题
header_bg = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(0), Inches(0), self.slide_width, Inches(1.2)
)
header_bg.fill.solid()
header_bg.fill.fore_color.rgb = self.colors['primary']
header_bg.line.fill.background()

self._add_text_box(
slide, Inches(0.8), Inches(0.2), Inches(11), Inches(0.8),
slide_data.get('title', ''), font_size=32,
color=self.colors['text_light'], bold=True
)

# 使用python-pptx原生图表
chart_data_info = slide_data.get('chart_data', {})
chart_type_str = slide_data.get('chart_type', 'bar')

chart_type_map = {
'bar': XL_CHART_TYPE.COLUMN_CLUSTERED,
'line': XL_CHART_TYPE.LINE_MARKERS,
'pie': XL_CHART_TYPE.PIE,
}
chart_type = chart_type_map.get(chart_type_str, XL_CHART_TYPE.COLUMN_CLUSTERED)

chart_data = CategoryChartData()
categories = chart_data_info.get('categories', ['A', 'B', 'C'])
chart_data.categories = categories

for series in chart_data_info.get('series', [{'name': '数据', 'values': [1,2,3]}]):
values = series.get('values', [0] * len(categories))
# 确保values长度与categories匹配
while len(values) < len(categories):
values.append(0)
chart_data.add_series(series.get('name', '系列'), values[:len(categories)])

chart = slide.shapes.add_chart(
chart_type, Inches(0.8), Inches(1.5), Inches(8), Inches(5),
chart_data
).chart

chart.has_legend = True
chart.legend.include_in_layout = False

# 洞察文本
insight = slide_data.get('insight', '')
if insight:
insight_box = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(9.2), Inches(1.5), Inches(3.8), Inches(3)
)
insight_box.fill.solid()
insight_box.fill.fore_color.rgb = self.colors['accent']
insight_box.line.fill.background()

tf = insight_box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = "💡 核心洞察"
p.font.size = Pt(16)
p.font.bold = True
p.font.color.rgb = self.colors['text_light']
p.font.name = "微软雅黑"

p2 = tf.add_paragraph()
p2.text = insight
p2.font.size = Pt(13)
p2.font.color.rgb = self.colors['text_light']
p2.font.name = "微软雅黑"

# 数据来源
source = slide_data.get('source', '')
if source:
self._add_text_box(
slide, Inches(0.8), Inches(6.8), Inches(8), Inches(0.4),
f"数据来源: {source}", font_size=10,
color=self.colors['secondary']
)

def build_summary_slide(self, slide_data: Dict):
"""构建总结页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
self._add_background(slide, self.colors['bg_dark'])

from pptx.enum.shapes import MSO_SHAPE

# 标题
self._add_text_box(
slide, Inches(1), Inches(0.5), Inches(11), Inches(1),
slide_data.get('title', '总结'), font_size=40,
color=self.colors['text_light'], bold=True,
alignment=PP_ALIGN.CENTER
)

# 分隔线
line = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(5.5), Inches(1.5), Inches(2.3), Inches(0.05)
)
line.fill.solid()
line.fill.fore_color.rgb = self.colors['accent']
line.line.fill.background()

# 核心要点
takeaways = slide_data.get('key_takeaways', [])
for i, point in enumerate(takeaways):
# 卡片式布局
card_width = Inches(3.5)
cards_per_row = 3
row = i // cards_per_row
col = i % cards_per_row

x = Inches(0.8) + col * (card_width + Inches(0.5))
y = Inches(2.2) + row * Inches(2.0)

card = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
x, y, card_width, Inches(1.5)
)
card.fill.solid()
card.fill.fore_color.rgb = self.colors['primary']
card.line.fill.background()

tf = card.text_frame
tf.word_wrap = True
tf.paragraphs[0].alignment = PP_ALIGN.CENTER

# 序号
p = tf.paragraphs[0]
p.text = f"0{i+1}"
p.font.size = Pt(24)
p.font.bold = True
p.font.color.rgb = self.colors['accent']
p.font.name = "微软雅黑"

# 内容
p2 = tf.add_paragraph()
p2.text = point
p2.font.size = Pt(14)
p2.font.color.rgb = self.colors['text_light']
p2.font.name = "微软雅黑"
p2.alignment = PP_ALIGN.CENTER

# 行动号召
cta = slide_data.get('call_to_action', '')
if cta:
self._add_text_box(
slide, Inches(1), Inches(6.2), Inches(11), Inches(0.8),
cta, font_size=20,
color=self.colors['accent'], bold=True,
alignment=PP_ALIGN.CENTER
)

def build_section_divider(self, section_title: str, section_number: int):
"""构建章节分隔页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
self._add_background(slide, self.colors['primary'])

from pptx.enum.shapes import MSO_SHAPE

# 大数字
self._add_text_box(
slide, Inches(1), Inches(1), Inches(3), Inches(2.5),
f"{section_number:02d}", font_size=96,
color=self.colors['accent'], bold=True
)

# 分隔线
line = slide.shapes.add_shape(
MSO_SHAPE.RECTANGLE,
Inches(1), Inches(3.8), Inches(3), Inches(0.05)
)
line.fill.solid()
line.fill.fore_color.rgb = self.colors['text_light']
line.line.fill.background()

# 章节标题
self._add_text_box(
slide, Inches(1), Inches(4.2), Inches(11), Inches(1.5),
section_title, font_size=40,
color=self.colors['text_light'], bold=True
)

def build_ppt(self, content: Dict, output_path: str):
"""根据生成的内容构建完整PPT"""

metadata = content.get('metadata', {})
outline = content.get('outline', {})
slides = content.get('slides', [])

# 1. 标题页
title_data = {
'title': metadata.get('title', ''),
'subtitle': metadata.get('subtitle', ''),
'author': '',
'date': ''
}
# 从slides中找title_slide数据
if slides and slides[0].get('layout_type') == 'title_slide':
title_data.update(slides[0])

self.build_title_slide(title_data)

# 2. 内容页
current_section = None
section_number = 0

for slide_data in slides:
layout_type = slide_data.get('layout_type', 'bullet_points')

# 跳过已处理的标题页
if layout_type == 'title_slide' and slide_data == slides[0]:
continue

# 章节分隔页
section = slide_data.get('section', '')
if section and section != current_section:
current_section = section
section_number += 1
self.build_section_divider(section, section_number)

# 根据布局类型选择构建方法
builder_map = {
'bullet_points': self.build_bullet_slide,
'content': self.build_bullet_slide,
'two_column': self.build_two_column_slide,
'chart': self.build_chart_slide,
'comparison': self.build_two_column_slide,
'summary': self.build_summary_slide,
'quote': self.build_bullet_slide,
'timeline': self.build_bullet_slide,
}

builder = builder_map.get(layout_type, self.build_bullet_slide)

try:
builder(slide_data)
except Exception as e:
print(f" ⚠️ 构建第 {slide_data.get('slide_number', '?')} 页时出错: {e}")
# 降级为基础bullet布局
self.build_bullet_slide(slide_data)

# 保存
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
self.prs.save(output_path)
print(f"\n✅ PPT已保存到: {output_path}")
```

#### `app.py` — 主程序入口

```python
import os
import json
import argparse
from datetime import datetime
from ppt_engine.content_generator import PPTContentGenerator
from ppt_engine.ppt_builder import PPTBuilder
from knowledge_base.kb_manager import KnowledgeBaseManager

def main():
parser = argparse.ArgumentParser(description='离线AI PPT生成器')
subparsers = parser.add_subparsers(dest='command')

# 知识库管理
kb_parser = subparsers.add_parser('kb', help='知识库管理')
kb_parser.add_argument('action', choices=['ingest', 'search', 'info'])
kb_parser.add_argument('--path', type=str, help='文档路径')
kb_parser.add_argument('--query', type=str, help='搜索查询')

# PPT生成
gen_parser = subparsers.add_parser('generate', help='生成PPT')
gen_parser.add_argument('--topic', type=str, required=True, help='PPT主题')
gen_parser.add_argument('--requirements', type=str, default='', help='额外要求')
gen_parser.add_argument('--slides', type=int, default=15, help='幻灯片数量')
gen_parser.add_argument('--template', type=str, default='business_blue',
choices=['business_blue', 'tech_dark', 'minimal_white'])
gen_parser.add_argument('--output', type=str, help='输出文件路径')

args = parser.parse_args()

if args.command == 'kb':
kb = KnowledgeBaseManager()
if args.action == 'ingest':
path = args.path or None
print("📚 开始导入文档到知识库...")
kb.ingest_documents(path)
elif args.action == 'search':
if not args.query:
print("请提供搜索查询 --query")
return
results = kb.search(args.query)
for r in results:
print(f"\n[相关度: {r['relevance_score']:.2f}]")
print(f"来源: {r['metadata'].get('source', '未知')}")
print(f"内容: {r['content'][:200]}...")
elif args.action == 'info':
print(f"知识库文档数: {kb.collection.count()}")

elif args.command == 'generate':
print("🚀 开始生成PPT...")
print(f" 主题: {args.topic}")
print(f" 页数: ~{args.slides}")
print(f" 模板: {args.template}")
print()

# 1. 生成内容
generator = PPTContentGenerator()
content = generator.generate_full_ppt_content(
topic=args.topic,
requirements=args.requirements,
num_slides=args.slides
)

# 保存中间JSON(方便调试和二次编辑)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
json_path = f"output/content_{timestamp}.json"
os.makedirs("output", exist_ok=True)
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(content, f, ensure_ascii=False, indent=2)
print(f"\n📄 内容JSON已保存: {json_path}")

# 2. 构建PPT
output_path = args.output or f"output/{args.topic}_{timestamp}.pptx"
builder = PPTBuilder(color_scheme=args.template)
builder.build_ppt(content, output_path)

print(f"\n🎉 PPT生成完成!")
print(f" 文件: {output_path}")
print(f" 页数: {content['metadata']['total_slides']}")

else:
parser.print_help()

if __name__ == '__main__':
main()
```

#### `web_ui.py` — Gradio Web 界面

```python
import gradio as gr
import json
import os
from datetime import datetime
from ppt_engine.content_generator import PPTContentGenerator
from ppt_engine.ppt_builder import PPTBuilder
from knowledge_base.kb_manager import KnowledgeBaseManager

generator = PPTContentGenerator()
kb = KnowledgeBaseManager()

def upload_documents(files):
"""上传文档到知识库"""
if not files:
return "请选择文件"

upload_dir = "knowledge_base/documents"
os.makedirs(upload_dir, exist_ok=True)

for file in files:
dest = os.path.join(upload_dir, os.path.basename(file.name))
os.rename(file.name, dest)

kb.ingest_documents(upload_dir)
return f"✅ 已导入 {len(files)} 个文档到知识库,当前共 {kb.collection.count()} 条记录"

def generate_ppt(topic, requirements, num_slides, template, progress=gr.Progress()):
"""生成PPT"""
if not topic:
return None, "请输入PPT主题"

progress(0.1, desc="生成大纲中...")
content = generator.generate_full_ppt_content(
topic=topic,
requirements=requirements,
num_slides=int(num_slides)
)

progress(0.7, desc="构建PPT文件...")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"output/{topic}_{timestamp}.pptx"
os.makedirs("output", exist_ok=True)

builder = PPTBuilder(color_scheme=template)
builder.build_ppt(content, output_path)

progress(1.0, desc="完成!")

outline_text = json.dumps(content['outline'], ensure_ascii=False, indent=2)
return output_path, f"✅ 生成完成!共 {content['metadata']['total_slides']} 页\n\n大纲预览:\n{outline_text[:2000]}"

# 构建界面
with gr.Blocks(title="离线AI PPT生成器", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎯 离线AI PPT生成器")
gr.Markdown("基于本地LLM + RAG知识库,完全离线生成高质量PPT")

with gr.Tab("📊 生成PPT"):
with gr.Row():
with gr.Column(scale=1):
topic_input = gr.Textbox(
label="PPT主题",
placeholder="例如:2024年度AI技术发展趋势分析报告",
lines=2
)
requirements_input = gr.Textbox(
label="额外要求(可选)",
placeholder="例如:面向技术管理层,重点关注大模型和Agent方向",
lines=3
)
with gr.Row():
slides_input = gr.Slider(
minimum=5, maximum=30, value=15, step=1,
label="幻灯片页数"
)
template_input = gr.Dropdown(
choices=["business_blue", "tech_dark", "minimal_white"],
value="business_blue",
label="配色方案"
)
generate_btn = gr.Button("🚀 开始生成", variant="primary", size="lg")

with gr.Column(scale=1):
output_file = gr.File(label="下载PPT")
output_info = gr.Textbox(label="生成信息", lines=15)

generate_btn.click(
fn=generate_ppt,
inputs=[topic_input, requirements_input, slides_input, template_input],
outputs=[output_file, output_info]
)

with gr.Tab("📚 知识库管理"):
gr.Markdown("上传文档到知识库,支持 PDF、Word、TXT、Markdown 格式")
file_upload = gr.File(
label="选择文档",
file_count="multiple",
file_types=[".pdf", ".docx", ".txt", ".md"]
)
upload_btn = gr.Button("📥 导入知识库")
upload_result = gr.Textbox(label="导入结果")

upload_btn.click(
fn=upload_documents,
inputs=[file_upload],
outputs=[upload_result]
)

gr.Markdown("### 🔍 知识库检索测试")
search_input = gr.Textbox(label="搜索查询")
search_btn = gr.Button("搜索")
search_result = gr.Textbox(label="搜索结果", lines=10)

def search_kb(query):
if not query:
return "请输入查询"
results = kb.search(query, top_k=5)
output = []
for i, r in enumerate(results, 1):
output.append(f"[{i}] 相关度: {r['relevance_score']:.2f}")
output.append(f" 来源: {r['metadata'].get('source', '未知')}")
output.append(f" 内容: {r['content'][:150]}...")
output.append("")
return "\n".join(output)

search_btn.click(fn=search_kb, inputs=[search_input], outputs=[search_result])

if __name__ == '__main__':
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
```

---

## 三、使用流程

### 1. 环境搭建

```bash
# 1. 安装Ollama并下载模型
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:32b-instruct-q4_K_M
ollama pull bge-m3

# 2. 创建项目环境
python -m venv venv
source venv/bin/activate # Linux/Mac
pip install -r requirements.txt

# 3. 下载嵌入模型(首次运行会自动下载,之后离线可用)
python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-m3', cache_folder='./models/embeddings')"
```

### 2. 构建知识库

```bash
# 将相关文档放入 knowledge_base/documents/ 目录
cp ~/my_reports/*.pdf knowledge_base/documents/
cp ~/my_docs/*.docx knowledge_base/documents/

# 导入知识库
python app.py kb ingest

# 测试检索
python app.py kb search --query "人工智能发展趋势"
```

### 3. 生成PPT

```bash
# 命令行方式
python app.py generate \
--topic "2024年人工智能技术发展趋势与企业应用策略" \
--requirements "面向CTO级别决策者,重点分析大模型、Agent和多模态方向,包含市场数据" \
--slides 20 \
--template business_blue

# Web界面方式
python web_ui.py
# 打开浏览器访问 http://localhost:7860
```

---

## 四、提升PPT质量的关键技巧

### 1. Prompt Engineering 优化

```python
# 在content_generator.py中,使用更精细的prompt

QUALITY_SYSTEM_PROMPT = """你是世界顶级的PPT设计咨询师,曾服务于麦肯锡、BCG等顶级咨询公司。

你的PPT内容遵循以下原则:
1. **金字塔原理**: 先结论后论据,每页一个核心观点
2. **MECE原则**: 分类完全穷尽、相互独立
3. **数据驱动**: 每个观点都有数据或案例支撑
4. **视觉思维**: 用对比、流程、矩阵等结构化方式呈现
5. **精炼表达**: 标题不超过10个字,要点不超过2行

你生成的每一页都应该能独立传达一个完整的信息。"""
```

### 2. 模板库扩展

```python
# 创建专业级PPT模板的关键要素
"""
templates/
├── business_blue.pptx # 商务蓝 - 适合正式汇报
├── tech_dark.pptx # 科技暗色 - 适合技术分享
├── minimal_white.pptx # 极简白 - 适合学术/创意
├── consulting_red.pptx # 咨询红 - 适合战略分析
└── data_green.pptx # 数据绿 - 适合数据报告

每个模板应包含:
- 预设的母版和布局
- 统一的字体方案
- 配色方案
- 占位符位置
- 页脚/页码样式
"""
```

### 3. 多轮优化 Pipeline

```python
class QualityOptimizer:
"""PPT内容质量优化器"""

def optimize_content(self, content: Dict) -> Dict:
"""多轮优化"""

# 第1轮:逻辑一致性检查
content = self._check_logic_consistency(content)

# 第2轮:数据准确性验证(基于知识库)
content = self._verify_data_accuracy(content)

# 第3轮:文字精炼
content = self._refine_text(content)

# 第4轮:视觉布局优化建议
content = self._optimize_layout(content)

return content

def _refine_text(self, content: Dict) -> Dict:
"""让LLM精炼每页文字"""
prompt = """请优化以下PPT页面的文字:

规则:
- 标题控制在8-12个字
- 每个要点不超过20个字
- 删除冗余修饰词
- 使用动词开头的短句
- 数字用阿拉伯数字

当前内容:
{content}

请返回优化后的JSON。"""
# ... 实现
```

---

## 五、硬件推荐配置

| 配置等级 | GPU | 内存 | 推荐模型 | 生成质量 |
|---------|-----|------|---------|---------|
| **入门** | 无GPU | 32GB RAM | Qwen2.5-7B (CPU) | ⭐⭐⭐ |
| **推荐** | RTX 4090 24GB | 64GB RAM | Qwen2.5-32B-Q4 | ⭐⭐⭐⭐ |
| **高端** | 2×RTX 4090 | 128GB RAM | Qwen2.5-72B-Q4 | ⭐⭐⭐⭐⭐ |
| **旗舰** | A100 80GB / H100 | 256GB RAM | DeepSeek-V2.5-236B | ⭐⭐⭐⭐⭐+ |

---

## 六、方案总结

```
┌─────────────────────────────────────────┐
│ 完整工作流程 │
│ │
│ 📚 文档 → 知识库(ChromaDB+BGE-M3) │
│ ↓ │
│ 🎯 用户输入主题和要求 │
│ ↓ │
│ 🔍 RAG检索相关知识 │
│ ↓ │
│ 📋 LLM生成大纲 (结构化JSON) │
│ ↓ │
│ 📝 LLM逐页生成详细内容 │
│ ↓ │
│ ✨ LLM质量优化和一致性检查 │
│ ↓ │
│ 🎨 python-pptx渲染为PPTX文件 │
│ ↓ │
│ 📊 高质量PPT输出 │
└─────────────────────────────────────────┘
```

**核心优势:**
- ✅ **完全离线**:所有组件本地运行,无需联网
- ✅ **知识驱动**:基于RAG确保内容专业、准确
- ✅ **高度可控**:中间JSON可人工编辑后再生成
- ✅ **模板化设计**:专业级视觉效果
- ✅ **可扩展**:易于添加新布局、新模板、新图表类型

 

 

 

claude sonnet

纯离线AI驱动PPT生成最佳解决方案

整体架构图

text
知识库文档 → 文档处理 → 向量数据库 → LLM推理 → PPT生成
     ↑              ↑           ↑          ↑         ↑
  本地文件      Ollama/      ChromaDB   本地模型   python-pptx
  PDF/Word     LangChain                Qwen/      或Marp

第一层:核心组件选型

1. 本地LLM推理引擎

text
推荐方案:Ollama(最简单易用)

备选:
├── llama.cpp(性能最优)
├── LM Studio(有GUI)
└── vLLM(高吞吐,需要GPU)

2. 推荐模型

场景模型显存需求
中文PPT(最佳) Qwen2.5:14b 10GB
平衡性能 Qwen2.5:7b 6GB
低配机器 Qwen2.5:3b 3GB
英文场景 Llama3.1:8b 6GB

3. 知识库栈

text
文档解析:LlamaParse / Unstructured.io(离线版)
向量模型:bge-m3(中文最强,纯离线)
向量数据库:ChromaDB / Qdrant(本地模式)
编排框架:LangChain / LlamaIndex

第二层:完整技术栈安装

Step 1:安装Ollama + 模型

Bash
# Linux/Mac
curl -fsSL https://ollama.ai/install.sh | sh

# 拉取模型(离线后无需网络)
ollama pull qwen2.5:14b
ollama pull nomic-embed-text  # 备用embedding

# 验证
ollama run qwen2.5:14b "你好"

Step 2:Python环境

Bash
conda create -n ppt-ai python=3.11
conda activate ppt-ai

pip install \
  langchain \
  langchain-community \
  langchain-ollama \
  chromadb \
  sentence-transformers \
  python-pptx \
  pymupdf \        # PDF解析
  python-docx \    # Word解析
  unstructured \   # 通用文档解析
  gradio           # 可选Web界面

Step 3:离线Embedding模型

Bash
# 提前下载到本地(关键!)
python -c "
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-m3')
model.save('./models/bge-m3')
print('下载完成')
"

第三层:核心代码实现

模块1:知识库构建

Python
# knowledge_base.py
import os
from pathlib import Path
from langchain_community.document_loaders import (
    PyMuPDFLoader, 
    Docx2txtLoader,
    TextLoader,
    DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

class KnowledgeBase:
    def __init__(self, kb_path="./knowledge_base", model_path="./models/bge-m3"):
        self.kb_path = kb_path
        
        # 离线加载embedding模型
        self.embeddings = HuggingFaceEmbeddings(
            model_name=model_path,
            model_kwargs={"device": "cuda"},  # 改为cpu如无GPU
            encode_kwargs={"normalize_embeddings": True}
        )
        
        self.vectorstore = None
    
    def load_documents(self, docs_dir: str):
        """加载多种格式文档"""
        documents = []
        docs_path = Path(docs_dir)
        
        loaders_map = {
            "*.pdf": PyMuPDFLoader,
            "*.docx": Docx2txtLoader,
            "*.txt": TextLoader,
            "*.md": TextLoader,
        }
        
        for pattern, loader_cls in loaders_map.items():
            for file_path in docs_path.glob(f"**/{pattern}"):
                try:
                    loader = loader_cls(str(file_path))
                    docs = loader.load()
                    # 添加来源元数据
                    for doc in docs:
                        doc.metadata["source_file"] = file_path.name
                    documents.extend(docs)
                    print(f"✅ 已加载: {file_path.name}")
                except Exception as e:
                    print(f"❌ 加载失败 {file_path.name}: {e}")
        
        return documents
    
    def build_index(self, docs_dir: str, persist_dir="./chroma_db"):
        """构建向量索引"""
        documents = self.load_documents(docs_dir)
        
        # 智能切分
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=512,
            chunk_overlap=64,
            separators=["\n\n", "\n", "。", "!", "?", ";", " ", ""]
        )
        chunks = splitter.split_documents(documents)
        print(f"📚 共切分 {len(chunks)} 个文本块")
        
        # 构建向量库
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=persist_dir
        )
        self.vectorstore.persist()
        print(f"✅ 知识库已构建并保存至 {persist_dir}")
    
    def load_existing(self, persist_dir="./chroma_db"):
        """加载已有知识库"""
        self.vectorstore = Chroma(
            persist_directory=persist_dir,
            embedding_function=self.embeddings
        )
        print(f"✅ 已加载现有知识库,共 {self.vectorstore._collection.count()} 条记录")
    
    def retrieve(self, query: str, top_k=5) -> list:
        """检索相关内容"""
        if not self.vectorstore:
            raise ValueError("请先构建或加载知识库")
        
        results = self.vectorstore.similarity_search_with_score(query, k=top_k)
        return [(doc.page_content, doc.metadata, score) for doc, score in results]

模块2:PPT结构生成器

Python
# ppt_generator.py
import json
from langchain_ollama import OllamaLLM
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain

class PPTStructureGenerator:
    def __init__(self, model="qwen2.5:14b"):
        self.llm = OllamaLLM(
            model=model,
            temperature=0.3,
            num_ctx=8192,
        )
    
    def generate_outline(self, topic: str, context: str, slides_count=12) -> dict:
        """生成PPT大纲"""
        
        prompt = PromptTemplate(
            input_variables=["topic", "context", "slides_count"],
            template="""你是专业的PPT设计师和内容策划专家。
            
基于以下知识库内容,为主题"{topic}"生成一个专业的PPT大纲。

【知识库参考内容】
{context}

【要求】
- 生成约{slides_count}张幻灯片
- 结构清晰:封面→目录→内容→总结→结束页
- 每页有明确标题和3-5个关键要点
- 内容必须基于知识库,不要编造
- 要点要精炼,每条不超过20字

【输出格式】严格按JSON输出,不要有任何其他内容:
{{
  "title": "PPT总标题",
  "subtitle": "副标题",
  "author": "作者/部门",
  "slides": [
    {{
      "slide_number": 1,
      "type": "cover",
      "title": "标题",
      "content": [],
      "notes": "演讲备注"
    }},
    {{
      "slide_number": 2,
      "type": "agenda",
      "title": "目录",
      "content": ["章节1", "章节2", "章节3"],
      "notes": ""
    }},
    {{
      "slide_number": 3,
      "type": "content",
      "title": "页面标题",
      "content": ["要点1", "要点2", "要点3"],
      "highlight": "核心数据或金句(可选)",
      "notes": "详细演讲稿"
    }}
  ]
}}"""
        )
        
        chain = LLMChain(llm=self.llm, prompt=prompt)
        result = chain.run(
            topic=topic,
            context=context,
            slides_count=slides_count
        )
        
        # 提取JSON
        try:
            # 找到JSON开始和结束位置
            start = result.find('{')
            end = result.rfind('}') + 1
            json_str = result[start:end]
            return json.loads(json_str)
        except json.JSONDecodeError as e:
            print(f"JSON解析错误: {e}")
            print(f"原始输出: {result}")
            raise
    
    def enrich_slide_content(self, slide: dict, context: str) -> dict:
        """深度丰富单张幻灯片内容"""
        
        prompt = PromptTemplate(
            input_variables=["slide_title", "points", "context"],
            template="""基于以下参考资料,为PPT页面"{slide_title}"生成详细内容。

参考资料:
{context}

当前要点:
{points}

请生成:
1. 更详细的要点说明(每条扩展为1-2句话)
2. 相关数据或案例(如果资料中有)
3. 演讲备注(200字左右的演讲稿)

JSON格式输出:
{{
  "detailed_points": ["详细要点1", "详细要点2"],
  "data_highlight": "关键数据(没有则为空)",
  "speaker_notes": "演讲备注内容"
}}"""
        )
        
        chain = LLMChain(llm=self.llm, prompt=prompt)
        result = chain.run(
            slide_title=slide.get("title", ""),
            points="\n".join(slide.get("content", [])),
            context=context
        )
        
        try:
            start = result.find('{')
            end = result.rfind('}') + 1
            enriched = json.loads(result[start:end])
            slide.update(enriched)
        except:
            pass
        
        return slide

模块3:PPT文件渲染器

Python
# ppt_renderer.py
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
from pptx.util import Inches, Pt
import copy

class PPTRenderer:
    """高质量PPT渲染器"""
    
    # 专业配色方案
    THEMES = {
        "blue_tech": {
            "primary": RGBColor(0x1A, 0x5F, 0xAB),     # 深蓝
            "secondary": RGBColor(0x00, 0xB4, 0xD8),    # 亮蓝
            "accent": RGBColor(0xFF, 0x6B, 0x35),        # 橙色
            "background": RGBColor(0xF8, 0xF9, 0xFA),   # 浅灰
            "text_dark": RGBColor(0x1A, 0x1A, 0x2E),    # 深色文字
            "text_light": RGBColor(0xFF, 0xFF, 0xFF),   # 白色文字
        },
        "dark_pro": {
            "primary": RGBColor(0x0D, 0x1B, 0x2A),
            "secondary": RGBColor(0x00, 0xF5, 0xFF),
            "accent": RGBColor(0xFF, 0xD6, 0x00),
            "background": RGBColor(0x16, 0x21, 0x3E),
            "text_dark": RGBColor(0xE0, 0xE0, 0xE0),
            "text_light": RGBColor(0xFF, 0xFF, 0xFF),
        },
        "green_nature": {
            "primary": RGBColor(0x2D, 0x6A, 0x4F),
            "secondary": RGBColor(0x52, 0xB7, 0x88),
            "accent": RGBColor(0xF7, 0xC5, 0x9F),
            "background": RGBColor(0xF0, 0xF7, 0xF4),
            "text_dark": RGBColor(0x1B, 0x40, 0x32),
            "text_light": RGBColor(0xFF, 0xFF, 0xFF),
        }
    }
    
    def __init__(self, theme="blue_tech"):
        self.prs = Presentation()
        self.theme = self.THEMES[theme]
        
        # 设置16:9比例
        self.prs.slide_width = Inches(13.33)
        self.prs.slide_height = Inches(7.5)
    
    def _add_background(self, slide, color: RGBColor):
        """添加背景色"""
        background = slide.background
        fill = background.fill
        fill.solid()
        fill.fore_color.rgb = color
    
    def _add_rectangle(self, slide, left, top, width, height, color, transparency=0):
        """添加矩形装饰"""
        from pptx.util import Inches
        shape = slide.shapes.add_shape(
            1,  # MSO_SHAPE_TYPE.RECTANGLE
            Inches(left), Inches(top),
            Inches(width), Inches(height)
        )
        shape.fill.solid()
        shape.fill.fore_color.rgb = color
        shape.line.fill.background()
        if transparency > 0:
            shape.fill.fore_color.theme_color = None
        return shape
    
    def _add_text(self, slide, text, left, top, width, height,
                  font_size=24, color=None, bold=False, 
                  align=PP_ALIGN.LEFT, font_name="微软雅黑"):
        """添加文本框"""
        txBox = slide.shapes.add_textbox(
            Inches(left), Inches(top),
            Inches(width), Inches(height)
        )
        tf = txBox.text_frame
        tf.word_wrap = True
        
        p = tf.paragraphs[0]
        p.alignment = align
        run = p.add_run()
        run.text = text
        
        font = run.font
        font.name = font_name
        font.size = Pt(font_size)
        font.bold = bold
        if color:
            font.color.rgb = color
        
        return txBox
    
    def render_cover(self, slide_data: dict):
        """渲染封面页"""
        slide_layout = self.prs.slide_layouts[6]  # 空白
        slide = self.prs.slides.add_slide(slide_layout)
        
        t = self.theme
        
        # 深色背景
        self._add_background(slide, t["primary"])
        
        # 顶部装饰条
        self._add_rectangle(slide, 0, 0, 13.33, 0.15, t["secondary"])
        
        # 底部装饰条
        self._add_rectangle(slide, 0, 7.35, 13.33, 0.15, t["accent"])
        
        # 左侧彩色竖条
        self._add_rectangle(slide, 0.5, 1.5, 0.08, 4, t["secondary"])
        
        # 主标题
        self._add_text(
            slide, slide_data.get("title", ""),
            1.0, 2.0, 10, 1.5,
            font_size=48, color=t["text_light"],
            bold=True, align=PP_ALIGN.LEFT
        )
        
        # 副标题
        self._add_text(
            slide, slide_data.get("subtitle", ""),
            1.0, 3.8, 9, 0.8,
            font_size=24, color=t["secondary"],
            bold=False, align=PP_ALIGN.LEFT
        )
        
        # 作者信息
        self._add_text(
            slide, slide_data.get("author", ""),
            1.0, 5.5, 5, 0.5,
            font_size=16, color=RGBColor(0xCC, 0xCC, 0xCC),
            align=PP_ALIGN.LEFT
        )
        
        return slide
    
    def render_content_slide(self, slide_data: dict, slide_num: int, total: int):
        """渲染内容页"""
        slide_layout = self.prs.slide_layouts[6]
        slide = self.prs.slides.add_slide(slide_layout)
        
        t = self.theme
        
        # 白色/浅色背景
        self._add_background(slide, t["background"])
        
        # 顶部标题栏
        self._add_rectangle(slide, 0, 0, 13.33, 1.4, t["primary"])
        
        # 顶部装饰条
        self._add_rectangle(slide, 0, 1.4, 13.33, 0.06, t["secondary"])
        
        # 标题文字
        self._add_text(
            slide, slide_data.get("title", ""),
            0.5, 0.2, 11, 1.0,
            font_size=32, color=t["text_light"],
            bold=True, align=PP_ALIGN.LEFT
        )
        
        # 页码
        self._add_text(
            slide, f"{slide_num} / {total}",
            11.5, 0.4, 1.5, 0.6,
            font_size=14, color=t["secondary"],
            align=PP_ALIGN.RIGHT
        )
        
        # 内容要点
        content_items = slide_data.get("content", [])
        y_start = 1.7
        
        for i, item in enumerate(content_items):
            # 要点编号圆圈
            num_shape = self._add_rectangle(
                slide, 0.5, y_start + i * 0.9, 0.35, 0.35,
                t["secondary"]
            )
            
            # 编号文字
            self._add_text(
                slide, str(i + 1),
                0.52, y_start + i * 0.9, 0.3, 0.35,
                font_size=12, color=t["text_light"],
                bold=True, align=PP_ALIGN.CENTER
            )
            
            # 要点内容
            self._add_text(
                slide, item,
                1.1, y_start + i * 0.88, 11.5, 0.7,
                font_size=20, color=t["text_dark"],
                align=PP_ALIGN.LEFT
            )
        
        # 高亮数据(如果有)
        highlight = slide_data.get("highlight") or slide_data.get("data_highlight")
        if highlight:
            self._add_rectangle(slide, 0.5, 6.5, 12.3, 0.7, t["accent"])
            self._add_text(
                slide, f"💡 {highlight}",
                0.7, 6.55, 12, 0.6,
                font_size=16, color=t["text_light"],
                bold=True, align=PP_ALIGN.LEFT
            )
        
        # 演讲备注
        notes = slide_data.get("notes") or slide_data.get("speaker_notes", "")
        if notes:
            slide.notes_slide.notes_text_frame.text = notes
        
        return slide
    
    def render_agenda(self, slide_data: dict):
        """渲染目录页"""
        slide_layout = self.prs.slide_layouts[6]
        slide = self.prs.slides.add_slide(slide_layout)
        
        t = self.theme
        self._add_background(slide, t["background"])
        self._add_rectangle(slide, 0, 0, 13.33, 1.4, t["primary"])
        self._add_rectangle(slide, 0, 1.4, 13.33, 0.06, t["secondary"])
        
        self._add_text(
            slide, "目  录",
            0.5, 0.2, 11, 1.0,
            font_size=36, color=t["text_light"],
            bold=True, align=PP_ALIGN.LEFT
        )
        
        items = slide_data.get("content", [])
        cols = 2 if len(items) > 4 else 1
        
        for i, item in enumerate(items):
            if cols == 2:
                col = i % 2
                row = i // 2
                x = 0.8 + col * 6.2
                y = 2.0 + row * 1.3
                w = 5.8
            else:
                x, y, w = 1.5, 2.0 + i * 1.0, 10.0
            
            # 序号
            self._add_rectangle(slide, x, y, 0.5, 0.5, t["secondary"])
            self._add_text(
                slide, f"0{i+1}",
                x+0.05, y+0.05, 0.4, 0.4,
                font_size=14, color=t["text_light"],
                bold=True, align=PP_ALIGN.CENTER
            )
            
            # 项目名
            self._add_text(
                slide, item,
                x+0.65, y+0.05, w-0.8, 0.5,
                font_size=20, color=t["text_dark"],
                bold=False
            )
        
        return slide
    
    def render_end_slide(self, title="感谢聆听"):
        """渲染结束页"""
        slide_layout = self.prs.slide_layouts[6]
        slide = self.prs.slides.add_slide(slide_layout)
        
        t = self.theme
        self._add_background(slide, t["primary"])
        self._add_rectangle(slide, 0, 0, 13.33, 0.15, t["secondary"])
        self._add_rectangle(slide, 0, 7.35, 13.33, 0.15, t["accent"])
        
        # 装饰圆(模拟几何感)
        self._add_rectangle(slide, 8, 1, 5, 5, t["secondary"])
        self._add_rectangle(slide, 8.3, 1.3, 4.4, 4.4, t["primary"])
        
        self._add_text(
            slide, title,
            1.0, 2.5, 8, 1.5,
            font_size=54, color=t["text_light"],
            bold=True, align=PP_ALIGN.LEFT
        )
        
        self._add_text(
            slide, "Q & A",
            1.0, 4.3, 6, 0.8,
            font_size=28, color=t["secondary"],
            align=PP_ALIGN.LEFT
        )
        
        return slide
    
    def build_ppt(self, ppt_data: dict, output_path: str):
        """完整构建PPT"""
        slides = ppt_data.get("slides", [])
        total_content = sum(1 for s in slides if s.get("type") == "content")
        content_count = 0
        
        for slide_data in slides:
            slide_type = slide_data.get("type", "content")
            
            if slide_type == "cover":
                slide_data["title"] = ppt_data.get("title", slide_data.get("title"))
                slide_data["subtitle"] = ppt_data.get("subtitle", slide_data.get("subtitle"))
                slide_data["author"] = ppt_data.get("author", slide_data.get("author"))
                self.render_cover(slide_data)
                
            elif slide_type == "agenda":
                self.render_agenda(slide_data)
                
            elif slide_type == "end":
                self.render_end_slide(slide_data.get("title", "感谢聆听"))
                
            else:  # content
                content_count += 1
                self.render_content_slide(slide_data, content_count, total_content)
        
        self.prs.save(output_path)
        print(f"✅ PPT已保存: {output_path}")
        return output_path

模块4:主控流程

Python
# main_pipeline.py
import argparse
from knowledge_base import KnowledgeBase
from ppt_generator import PPTStructureGenerator
from ppt_renderer import PPTRenderer

class PPTMakerPipeline:
    def __init__(self, 
                 model="qwen2.5:14b",
                 theme="blue_tech",
                 embedding_model="./models/bge-m3"):
        
        print("🚀 初始化系统...")
        self.kb = KnowledgeBase(model_path=embedding_model)
        self.generator = PPTStructureGenerator(model=model)
        self.theme = theme
    
    def setup_knowledge_base(self, docs_dir=None, db_dir="./chroma_db"):
        """设置知识库"""
        if docs_dir:
            print(f"📚 构建知识库: {docs_dir}")
            self.kb.build_index(docs_dir, db_dir)
        else:
            print("📂 加载现有知识库...")
            self.kb.load_existing(db_dir)
    
    def run(self, topic: str, output_path: str, slides_count=15):
        """执行完整流程"""
        print(f"\n🎯 生成主题: {topic}")
        print("=" * 50)
        
        # 1. 检索相关知识
        print("🔍 检索知识库...")
        retrieval_results = self.kb.retrieve(topic, top_k=8)
        
        context = "\n\n---\n\n".join([
            f"[来源: {meta.get('source_file', '未知')}]\n{content}"
            for content, meta, score in retrieval_results
            if score < 1.0  # 过滤低相关性
        ])
        
        print(f"📄 检索到 {len(retrieval_results)} 条相关内容")
        
        # 2. 生成PPT结构
        print("🤖 LLM生成PPT大纲...")
        ppt_data = self.generator.generate_outline(
            topic=topic,
            context=context,
            slides_count=slides_count
        )
        print(f"📋 生成 {len(ppt_data['slides'])} 张幻灯片结构")
        
        # 3. 丰富内容页(可选,提升质量但耗时)
        print("✨ 深度丰富内容...")
        for i, slide in enumerate(ppt_data["slides"]):
            if slide.get("type") == "content":
                # 针对每页内容做二次检索
                slide_context = self.kb.retrieve(
                    slide.get("title", topic), top_k=3
                )
                mini_context = "\n".join([c for c, _, _ in slide_context])
                ppt_data["slides"][i] = self.generator.enrich_slide_content(
                    slide, mini_context
                )
                print(f"  ✓ 第 {i+1} 页处理完成")
        
        # 4. 渲染PPT
        print("🎨 渲染PPT文件...")
        renderer = PPTRenderer(theme=self.theme)
        renderer.build_ppt(ppt_data, output_path)
        
        return ppt_data


# 命令行入口
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="离线AI PPT生成器")
    parser.add_argument("topic", help="PPT主题")
    parser.add_argument("--docs", help="知识库文档目录")
    parser.add_argument("--output", default="output.pptx", help="输出文件")
    parser.add_argument("--model", default="qwen2.5:14b", help="LLM模型")
    parser.add_argument("--slides", type=int, default=12, help="幻灯片数量")
    parser.add_argument("--theme", default="blue_tech", 
                        choices=["blue_tech", "dark_pro", "green_nature"])
    args = parser.parse_args()
    
    pipeline = PPTMakerPipeline(model=args.model, theme=args.theme)
    pipeline.setup_knowledge_base(docs_dir=args.docs)
    pipeline.run(
        topic=args.topic,
        output_path=args.output,
        slides_count=args.slides
    )

第四层:Web界面(可选)

Python
# web_ui.py - Gradio界面
import gradio as gr
import os
from main_pipeline import PPTMakerPipeline

pipeline = None

def init_system(model_name, theme, docs_dir):
    global pipeline
    pipeline = PPTMakerPipeline(model=model_name, theme=theme)
    if docs_dir and os.path.exists(docs_dir):
        pipeline.setup_knowledge_base(docs_dir=docs_dir)
        return "✅ 系统初始化完成,知识库已加载"
    return "⚠️ 系统初始化完成,未加载知识库"

def generate_ppt(topic, slides_count, output_name):
    if not pipeline:
        return None, "❌ 请先初始化系统"
    
    output_path = f"./output/{output_name}.pptx"
    os.makedirs("./output", exist_ok=True)
    
    try:
        pipeline.run(topic, output_path, slides_count)
        return output_path, f"✅ PPT生成成功!共 {slides_count} 张"
    except Exception as e:
        return None, f"❌ 生成失败: {str(e)}"

with gr.Blocks(title="离线AI PPT生成器", theme=gr.themes.Soft()) as app:
    gr.Markdown("# 🎯 离线AI PPT生成器")
    
    with gr.Row():
        with gr.Column():
            gr.Markdown("### ⚙️ 系统配置")
            model_select = gr.Dropdown(
                choices=["qwen2.5:14b", "qwen2.5:7b", "qwen2.5:3b"],
                value="qwen2.5:14b", label="选择模型"
            )
            theme_select = gr.Dropdown(
                choices=["blue_tech", "dark_pro", "green_nature"],
                value="blue_tech", label="主题风格"
            )
            docs_dir = gr.Textbox(label="知识库目录", value="./docs")
            init_btn = gr.Button("初始化系统", variant="primary")
            init_status = gr.Textbox(label="状态", interactive=False)
        
        with gr.Column():
            gr.Markdown("### 🎨 生成PPT")
            topic_input = gr.Textbox(label="PPT主题", placeholder="例如:人工智能发展现状与趋势")
            slides_num = gr.Slider(5, 25, value=12, step=1, label="幻灯片数量")
            output_name = gr.Textbox(label="输出文件名", value="my_presentation")
            gen_btn = gr.Button("🚀 生成PPT", variant="primary")
            gen_status = gr.Textbox(label="生成状态", interactive=False)
            download_file = gr.File(label="下载PPT")
    
    init_btn.click(init_system, [model_select, theme_select, docs_dir], init_status)
    gen_btn.click(generate_ppt, [topic_input, slides_num, output_name], 
                  [download_file, gen_status])

app.launch(server_name="0.0.0.0", server_port=7860, share=False)

第五层:一键启动脚本

Bash
#!/bin/bash
# start.sh

echo "🚀 启动离线AI PPT生成系统"

# 检查Ollama
if ! command -v ollama &> /dev/null; then
    echo "❌ Ollama未安装"
    exit 1
fi

# 启动Ollama服务
ollama serve &
sleep 2

# 命令行模式
if [ "$1" = "cli" ]; then
    python main_pipeline.py \
        "$2" \
        --docs ./docs \
        --output "./output/$2.pptx" \
        --model qwen2.5:14b \
        --slides 15
        
# Web界面模式
else
    echo "🌐 启动Web界面: http://localhost:7860"
    python web_ui.py
fi

进阶优化方案

图表自动生成

Python
# 使用matplotlib生成图表并插入PPT
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')  # 无GUI模式
import io
from pptx.util import Inches

def add_chart_to_slide(slide, data: dict, chart_type="bar"):
    """生成图表并插入幻灯片"""
    fig, ax = plt.subplots(figsize=(8, 4))
    
    if chart_type == "bar":
        ax.bar(data["labels"], data["values"], color="#1A5FAB")
    elif chart_type == "line":
        ax.plot(data["labels"], data["values"], marker='o', color="#1A5FAB")
    elif chart_type == "pie":
        ax.pie(data["values"], labels=data["labels"], autopct='%1.1f%%')
    
    ax.set_title(data.get("title", ""), fontsize=14)
    plt.tight_layout()
    
    # 转为字节流
    img_bytes = io.BytesIO()
    plt.savefig(img_bytes, format='png', dpi=150, bbox_inches='tight')
    img_bytes.seek(0)
    plt.close()
    
    # 插入PPT
    slide.shapes.add_picture(img_bytes, Inches(7), Inches(1.8), 
                              Inches(6), Inches(4))

质量评估

Python
def evaluate_ppt_quality(ppt_data: dict) -> dict:
    """评估生成质量"""
    scores = {}
    slides = ppt_data.get("slides", [])
    
    # 内容完整性
    content_slides = [s for s in slides if s.get("type") == "content"]
    scores["completeness"] = min(len(content_slides) / 8 * 100, 100)
    
    # 要点数量
    avg_points = sum(len(s.get("content", [])) for s in content_slides) / max(len(content_slides), 1)
    scores["content_density"] = min(avg_points / 4 * 100, 100)
    
    # 备注完整性
    has_notes = sum(1 for s in slides if s.get("notes") or s.get("speaker_notes"))
    scores["notes_coverage"] = has_notes / max(len(slides), 1) * 100
    
    overall = sum(scores.values()) / len(scores)
    scores["overall"] = overall
    
    print(f"📊 质量评分: {overall:.1f}/100")
    for k, v in scores.items():
        print(f"  {k}: {v:.1f}")
    
    return scores

硬件配置建议

配置GPU模型选择生成速度
高配 RTX 4090 24G Qwen2.5:14b ~3分钟/PPT
中配 RTX 3080 10G Qwen2.5:7b ~5分钟/PPT
低配 无GPU/4G Qwen2.5:3b ~15分钟/PPT
服务器 A100 80G Qwen2.5:72b ~5分钟/PPT(最高质量)

文件目录结构

text
ppt-ai/
├── models/
│   └── bge-m3/          # 离线embedding模型
├── docs/                 # 知识库文档
│   ├── *.pdf
│   ├── *.docx
│   └── *.txt
├── chroma_db/            # 向量数据库(自动生成)
├── output/               # 生成的PPT
├── knowledge_base.py
├── ppt_generator.py
├── ppt_renderer.py
├── main_pipeline.py
├── web_ui.py
└── start.sh

快速开始命令

Bash
# 1. 安装依赖
pip install -r requirements.txt

# 2. 构建知识库并生成PPT
python main_pipeline.py "人工智能在医疗领域的应用" \
    --docs ./my_docs \
    --output ./output/ai_medical.pptx \
    --slides 15 \
    --theme blue_tech

# 3. 或启动Web界面
python web_ui.py

这套方案的核心优势是完全离线、质量高、可扩展,所有组件均可在无网络环境下稳定运行。

 

 

chatgpt

要实现**“纯离线 + LLM + 知识库 → 自动生成高质量 PPT”,核心是搭建一个本地 AI 内容生产流水线(Pipeline),而不是单点工具。下面给你一套工程级最佳方案(可落地)**。


---

一、整体架构(离线闭环)

这是你要实现的核心系统结构:

本地知识库 → 向量检索(RAG)

本地大模型(LLM)

PPT结构生成(大纲 + 文案)

PPT渲染引擎(模板 + 排版)

导出 .pptx


---

二、核心组件选型(全离线最佳组合)

1️⃣ 本地大模型(LLM)

推荐优先级:

第一选择(中文+专业能力强)

Qwen2.5(7B / 14B / 32B)


第二选择(稳定+推理强)

LLaMA 3(8B / 70B)


第三选择(轻量快速)

Mistral(7B)

 

👉 部署工具:

Ollama(最简单)

vLLM(高并发)

LM Studio(调试方便)


📌 建议:

你有8卡服务器 → 直接上 Qwen2.5-32B / 72B

单机 → Qwen2.5-14B + 量化(INT4)

 

---

2️⃣ 本地知识库(RAG)

核心:让PPT内容“有依据、有深度”

推荐组合:

向量库:

FAISS(轻量)

Milvus(企业级)


Embedding模型:

bge-large-zh / bge-m3(强烈推荐)


框架:

LangChain

或 LlamaIndex

 

📌 你的知识库来源:

PDF / Word / PPT

数据库

手册(你现在做的应急系统手册非常适合)

 

---

3️⃣ PPT生成引擎(关键)

这里是核心差异点,很多人做不好。

✅ 推荐方案:结构化生成 + 模板渲染

方案A(最稳)

python-pptx


👉 优点:

完全离线

可控排版

可企业级定制

 

---

✅ 推荐架构(重要)

LLM 不直接生成 PPT
👉 LLM 生成 JSON → 程序渲染 PPT

示例:

{
"title": "应急救援系统介绍",
"slides": [
{
"type": "title",
"title": "系统概述",
"subtitle": "背景与目标"
},
{
"type": "content",
"title": "核心功能",
"points": [
"指挥调度",
"实时监控",
"数据分析"
]
}
]
}

👉 然后用 python-pptx 渲染:

from pptx import Presentation

prs = Presentation()
slide_layout = prs.slide_layouts[1]

slide = prs.slides.add_slide(slide_layout)
title = slide.shapes.title
content = slide.placeholders[1]

title.text = "核心功能"
content.text = "指挥调度\n实时监控\n数据分析"

prs.save("output.pptx")


---

4️⃣ 模板系统(决定“高级感”)

高质量PPT ≠ 内容好
👉 关键是 模板 + 排版规则

你需要:

统一母版(公司风格)

多种布局:

标题页

目录页

内容页

图表页

对比页

 

📌 做法:

用 PowerPoint 设计 .pptx 模板

python-pptx 读取模板进行填充

 

---

5️⃣ 图表 & 可视化(加分项)

Matplotlib

ECharts(离线版)


👉 LLM生成数据 → 程序生成图 → 插入PPT


---

三、完整流程(工业级)

Step 1:知识处理

文档 → 切块(chunk)

向量化 → 存入 FAISS

 

---

Step 2:生成PPT大纲

Prompt示例:

你是资深咨询顾问,请基于知识库生成一个PPT大纲:
要求:
- 逻辑清晰(总-分-总)
- 每页不超过5点
- 适合汇报


---

Step 3:生成结构化JSON

输出JSON格式:
- slide_type
- title
- content
- notes


---

Step 4:PPT渲染

根据 slide_type 选择模板

填充内容

自动分页(避免一页过多)

 

---

Step 5:优化增强(高级)

自动加:

图标(icon)

配图(离线素材库)

图表

 


---

四、增强能力(让PPT“像人做的”)

1️⃣ 风格控制

Prompt加入:

“麦肯锡风格”

“政府汇报风格”

“产品发布风格”

 

---

2️⃣ 内容压缩

避免“AI废话”:

👉 加规则:

每点 ≤ 15字

动词开头

去废话

 

---

3️⃣ 自动分页算法(关键)

规则:

if 内容 > 5条:
自动拆分为2页


---

五、推荐完整技术栈(最佳实践)

模块 技术

LLM Qwen2.5
推理框架 Ollama / vLLM
知识库 FAISS + bge
编排 LangChain
PPT生成 python-pptx
图表 Matplotlib
UI(可选) Vue / React

 

---

六、进阶架构(你这种工程师建议做)

你可以做成一个系统:

👉 “离线PPT生成平台”

模块:

知识库管理

Prompt管理

PPT模板管理

一键生成PPT

可编辑预览

 

---

七、关键难点(必须注意)

⚠️ 1. LLM不要直接生成PPT

→ 必须 JSON 中间层


---

⚠️ 2. 模板决定80%质量

→ 一定要专业设计


---

⚠️ 3. 内容必须RAG

→ 否则都是“废话PPT”


---

八、一句话总结(架构本质)

👉 本质不是“AI生成PPT”
👉 而是:

“RAG + LLM + 结构化输出 + 模板渲染”


---

如果你要更进一步

我可以帮你直接做:

✅ 完整项目架构(代码级)
✅ Prompt工程设计(企业级)
✅ PPT模板体系(咨询级)
✅ 一键生成系统(带UI)

直接说你要做到哪一步,我给你“可运行方案”。

 

用 Dify 纯离线生成高质量 PPT是完全可行的,但关键点在于:
👉 Dify 只负责“智能生成”,真正的 PPT 必须由外部程序渲染

下面给你一套工程级最佳落地方案(基于 Dify + 本地模型 + PPT渲染服务)


---

一、整体架构(Dify离线PPT系统)

本地知识库(Dify Dataset)

Dify(RAG + Prompt)

输出结构化 JSON(PPT结构)

本地PPT服务(python-pptx)

生成 .pptx 文件


---

二、Dify离线部署(核心前提)

1️⃣ 部署 Dify(完全离线)

Dify 是开源的,可以本地部署:

Docker 部署

禁用外部 API

使用本地模型

 

---

2️⃣ 接入本地 LLM

推荐组合:

Ollama(最简单)

模型:

Qwen2.5(强烈推荐)

或 LLaMA 3

 

👉 Dify 配置:

Model Provider → OpenAI-Compatible
Base URL → http://localhost:11434/v1
Model → qwen:14b


---

3️⃣ 配置知识库(RAG)

Dify 内置 Dataset:

上传:

PDF(你现在的应急系统手册)

Word

技术文档

 

👉 Embedding(必须本地):

bge-large-zh


👉 检索策略:

TopK:3~5

Chunk:500~1000 tokens

 

---

三、关键设计:PPT生成必须“结构化输出”

这是成败关键。


---

❌ 错误方式(很多人踩坑)

直接让 Dify 输出 PPT 内容

问题:

无法排版

内容混乱

不可控

 

---

✅ 正确方式(工业级)

👉 让 Dify 输出 JSON


---

1️⃣ Dify Prompt(核心模板)

你是顶级咨询顾问,请生成PPT内容。

要求:
1. 输出JSON格式
2. 每页不超过5个要点
3. 语言简洁、专业
4. 适合汇报场景

JSON格式如下:

{
"title": "PPT标题",
"slides": [
{
"type": "title",
"title": "",
"subtitle": ""
},
{
"type": "content",
"title": "",
"points": []
}
]
}


---

2️⃣ Dify Workflow(推荐用)

用 Workflow 而不是 Chat App:

流程:

用户输入 → 知识库检索 → LLM生成 → JSON输出


---

四、PPT生成服务(必须自建)

Dify 本身不能生成 PPT 文件
👉 必须接一个服务


---

方案:python-pptx 服务

使用 python-pptx


---

1️⃣ 写一个 API 服务

例如:

from fastapi import FastAPI
from pptx import Presentation

app = FastAPI()

@app.post("/generate-ppt")
def generate_ppt(data: dict):
prs = Presentation("template.pptx")

for slide in data["slides"]:
layout = prs.slide_layouts[1]
s = prs.slides.add_slide(layout)
s.shapes.title.text = slide["title"]
s.placeholders[1].text = "\n".join(slide.get("points", []))

file_path = "output.pptx"
prs.save(file_path)

return {"file": file_path}


---

2️⃣ Dify 调用该服务

用:

HTTP Request Node(Workflow)


流程:

LLM(JSON) → HTTP请求 → PPT服务 → 返回文件路径


---

五、高质量PPT的关键(重点)

1️⃣ 模板(决定80%质量)

👉 必须准备:

咨询风格模板(类似麦肯锡)

政府汇报模板

科技风模板


👉 做法:

用 PowerPoint 做好母版

python-pptx 加载模板

 

---

2️⃣ 内容优化 Prompt

加入约束:

- 每点 ≤ 12字
- 使用动词开头
- 禁止空话


---

3️⃣ 自动分页逻辑(关键)

在 Prompt 或后处理:

if len(points) > 5:
拆分为多个slide


---

4️⃣ 图表增强(进阶)

可扩展:

数据 → matplotlib → 插入PPT

架构图 → 本地图标库

 

---

六、完整执行流程

① 用户输入:生成应急系统PPT

② Dify:
- 检索知识库
- 生成结构化JSON

③ 调用本地API:
/generate-ppt

④ 返回:
output.pptx


---

七、进阶架构(你适合做)

你可以做成一个系统:

👉 “Dify + PPT生成平台”

模块:

知识库管理

Prompt管理

模板管理

PPT生成

下载中心

 

---

八、你这个场景的最佳配置(直接给答案)

考虑你:

有知识库

做系统开发

想企业级


👉 最优方案:

Dify(Workflow)
+ Ollama(Qwen2.5 14B/32B)
+ Dify Dataset(RAG)
+ FastAPI(PPT生成服务)
+ python-pptx(渲染)
+ 专业模板(核心)


---

九、一句话总结

👉 Dify 负责“写内容”
👉 python-pptx 负责“做PPT”

两者必须解耦。


---

如果你要更深入

我可以直接帮你做:

✅ Dify Workflow JSON(可导入)

✅ PPT生成服务完整代码

✅ 高质量Prompt模板(咨询级)

✅ PPT模板设计规范(直接提升档次)


你下一步可以说:
👉 “给我完整项目代码” 或 “给我Workflow配置”

 

 

grok

**最佳解决方案:使用 Presenton 开源 AI 演示文稿生成器 + Ollama 本地 LLM + 文档上传(实现知识库功能)**

这是目前**纯离线、最简单、高质量、开箱即用**的方式,完全满足你的需求:
- **纯离线**:Ollama 运行本地 LLM(如 Llama 3.2、DeepSeek-R1、Qwen2.5 等),生成过程无需联网。
- **AI LLM + 知识库**:支持直接上传 PDF、DOCX、PPTX、TXT 等文档作为知识库,LLM 会自动参考这些内容生成幻灯片(类似轻量 RAG)。
- **高质量 PPT**:直接输出原生 **.pptx** 文件(可在 PowerPoint 或 WPS 中编辑),支持专业模板、目录、标题页、图片占位符、自定义语气/长度/语言。
- **开源免费**:Apache 2.0 协议,无订阅、无数据泄露。

Presenton 是专为本地生成的开源工具(GitHub: presenton/presenton),被社区公认为本地 AI 生成 PPT 的最佳选择之一,比手动写 Python 脚本更友好,比在线工具(如 Gamma)更隐私。

### 1. 硬件与前置要求(一次性准备)
- **电脑配置**:推荐 16GB+ 内存 + NVIDIA GPU(显存 8GB+ 更好)。CPU 也能跑,但生成速度慢。
- **安装 Ollama**(本地 LLM 引擎):
1. 官网下载安装(https://ollama.com)—— Windows/Mac/Linux 都支持。
2. 命令行拉取推荐模型(一次下载,后续纯离线):
```
ollama pull llama3.2 # 轻量快速(3B/8B 推荐)
# 或 ollama pull deepseek-r1:8b / qwen2.5:14b(中文能力更强)
```
建议先用 8B-14B 模型,平衡质量与速度。

### 2. 安装 Presenton(推荐 Docker,最简单纯离线)
**最推荐方式(5 分钟搞定)**:
```bash
# 1. 安装 Docker(如果没有)
# Windows/Mac:直接下载 Docker Desktop

# 2. 一条命令启动(带 Ollama 支持)
docker run -it --name presenton \
-p 5000:80 \
-e LLM="ollama" \
-e OLLAMA_MODEL="llama3.2" \ # 改成你拉取的模型名
-e IMAGE_PROVIDER="none" \ # 纯离线关闭在线图片(或后面配 ComfyUI)
-v "./app_data:/app_data" \
ghcr.io/presenton/presenton:latest
```

- 访问 http://localhost:5000 即可看到网页界面。
- **GPU 支持**:加 `--gpus=all`(需先装 NVIDIA Container Toolkit)。
- 想用桌面版?克隆 GitHub 后用 Electron 打包(Node.js 环境),但 Docker 更稳。

**完全离线提示**:
- 关闭 web search 和在线图片生成(IMAGE_PROVIDER=none)。
- 图片可后续手动添加,或额外部署本地 ComfyUI(Stable Diffusion)作为 image provider。

### 3. 使用流程(生成高质量 PPT)
1. **准备知识库**:把你的文档(PDF、Word、现有 PPT、笔记等)拖进界面上传(支持批量)。
2. **输入提示**(中文直接支持):
- 示例提示(效果极佳):
```
使用上传的[你的文档名]作为知识库,生成一份专业的高质量 PPT。
主题:XXX(你的主题)
目标观众:XXX
共 12-15 页,包含标题页、目录、每页关键点、数据图表描述、总结页。
风格:现代简洁、专业配色、每页不超过 5 行文字。
语言:简体中文
```
3. **点击生成** → 自动输出 .pptx 文件(保存在 app_data 目录)。
4. **打开 PowerPoint/WPS 编辑**:所有文字、布局都是原生元素,可直接修改。

**进阶技巧**(让质量更高):
- 用**更强本地模型**(如 Qwen2.5-32B 或 DeepSeek-R1)生成内容更准确。
- 先在 Ollama 里单独聊天测试提示词(让 LLM 输出结构化 Markdown),再喂给 Presenton。
- 支持自定义模板(上传已有 .pptx 作为设计模板,保持品牌一致)。

### 4. 如果你想要更强的知识库(完整 RAG)
Presenton 的文档上传已够用(LLM 会直接参考)。
若知识库很大(上百份文档),可**额外搭配 AnythingLLM**(另一个纯离线 RAG 工具):
- 安装 AnythingLLM(Docker 一键)。
- 用 AnythingLLM + Ollama 构建向量知识库 → 查询后把答案复制到 Presenton 提示中。
- 或用 AnythingLLM + Python 脚本(python-pptx)实现全自动(进阶玩家)。

### 5. 其他优秀备选方案(按易用性排序)
| 方案 | 易用性 | RAG/知识库 | 输出 | 推荐场景 | 缺点 |
|------|--------|------------|------|----------|------|
| **Presenton**(首推) | ★★★★★ | 支持文档上传 | 原生 PPTX | 大多数人 | 图片需本地化 |
| **Powerpointer-For-Local-LLMs**(GitHub: CyberTimon/Powerpointer-For-Local-LLMs) | ★★★★ | 无(可手动喂 RAG 输出) | 原生 PPTX + 7 种模板 | 想极简 Python 脚本 | 无 UI,需配置 API |
| **自建 Python 脚本**(Ollama + LangChain + python-pptx + ChromaDB) | ★★★ | 完整 RAG | 原生 PPTX | 极客/高度定制 | 需要写代码 |
| **AnythingLLM + 自定义脚本** | ★★★★ | 最佳 RAG | PPTX | 超大知识库 | 需二次开发 |

**自建脚本核心思路**(仅供参考):
- Ollama 提供 LLM。
- LangChain + Chroma/FAISS 做本地向量知识库。
- LLM 输出结构化 JSON(标题、内容、布局)。
- `python-pptx` 库自动组装幻灯片 + Streamlit 做 UI。

### 总结 & 建议
- **立即上手**:先装 Ollama + Docker 跑 Presenton,5-10 分钟就能生成第一份离线 PPT。
- 生成质量关键:**好提示 + 好模型 + 好知识文档**。
- 后续优化:配 ComfyUI 实现本地 AI 图片自动插入;用更大模型提升中文表达。

需要我给你 **Presenton 的详细 Docker 命令 + 最佳提示词模板**,或者 **Powerpointer 的完整 Python 代码示例**,或者 **完整自建 RAG + python-pptx 教程**,随时告诉我!
这样一套方案完全私有、高质量、零成本、纯离线,完美替代所有在线 AI PPT 工具。

 

gemini

在纯离线环境下,结合大语言模型(LLM)和本地知识库生成高质量的PPT,是一个非常典型且极具价值的企业级/个人效率需求。这不仅能保证数据绝对隐私,还能根据你自己的文档库定制内容。

要实现这个目标,最佳的架构是 RAG(检索增强生成) + 结构化输出 + 本地渲染工具

以下是为你梳理的纯离线、高质量 PPT 生成的最佳落地解决方案:

核心架构蓝图

一个完整的离线 PPT 生成流水线包含三个独立且必须完全本地化的模块:

  1. 本地知识库引擎 (RAG):负责读取你的本地文档(PDF、Word、TXT),进行向量化存储,并根据你的主题检索相关片段。

  2. 本地大语言模型 (LLM):负责理解检索到的知识,提炼逻辑框架,并严格按照特定格式(如 Markdown 或 JSON)输出 PPT 大纲和每页的内容。

  3. 本地 PPT 渲染器:读取 LLM 输出的结构化文本,结合预设的 PPT 模板,自动生成 .pptx 文件。


推荐的离线工具栈(最优解)

为了兼顾部署难度和生成质量,推荐以下开源免费的工具组合:

  • 本地模型运行框架: Ollama (极其轻量,支持 Mac/Windows/Linux,API 接口标准化)。

  • 推荐 LLM 模型: Qwen2.5-7B-InstructQwen2.5-14B(千问模型在中文理解、逻辑排版和指令遵循方面表现极佳)。

  • 本地向量模型 (Embedding): bge-m3bge-large-zh-v1.5(专为中文优化的本地向量模型,Ollama 可直接拉取)。

  • 知识库搭建框架: AnythingLLM(开箱即用的本地 RAG 客户端) 或使用 LangChain 自己写脚本。

  • PPT 渲染工具: Marp CLI(基于 Markdown 生成极其优雅的幻灯片) 或 python-pptx(适合需要套用公司复杂模板的场景)。


具体落地实施步骤

第一步:部署本地基座与大模型

  1. 下载并安装 Ollama

  2. 在命令行中拉取并运行中文能力强的大模型:

    Bash
     
    ollama run qwen2.5:14b
    
  3. 拉取本地向量模型(用于知识库检索):

    Bash
     
    ollama pull bge-m3
    

第二步:构建本地离线知识库

如果你不想写代码,直接下载 AnythingLLM 桌面版(支持纯离线):

  1. 在 AnythingLLM 的设置中,将 LLM 提供商向量数据库提供商 都指向你的本地 Ollama 及其对应的模型。

  2. 创建一个 Workspace(工作区),把你想要作为 PPT 背景资料的文档(如项目报告、产品手册)拖进去。

  3. AnythingLLM 会在本地将文档切片并向量化。

第三步:设计生成 PPT 的核心提示词 (Prompt)

在生成阶段,你需要向 LLM 下达明确的指令,让它基于知识库内容生成特定格式的 Markdown。这是最关键的一步。

示例 Prompt(基于 Marp 格式):

"你是一个资深的商业咨询顾问。请基于本地知识库中关于【XXX项目】的内容,为我生成一份高质量的PPT。

要求:

  1. 结构化呈现:包含封面、目录、行业痛点、解决方案、核心优势和总结。

  2. 每页字数精简,多用短句和项目符号(Bullet points)。

  3. 必须严格使用 Marp Markdown 格式输出,每页幻灯片之间用 --- 分隔。

请遵循以下模板结构输出:

[幻灯片标题]

[副标题或核心观点]

  • 要点 1

  • 要点 2

    ---"

第四步:离线渲染成 PPTX 文件

LLM 会输出一段标准的 Markdown 代码。你可以将其保存为 presentation.md

接下来,使用 Marp 将其离线渲染:

  1. 如果你使用 VS Code,可以安装 Marp for VS Code 插件,直接在编辑器里预览,并右键点击 Export slide deck 选择导出为 .pptx.pdf

  2. 如果你希望完全自动化,可以通过 Node.js 安装 Marp CLI:

    Bash
     
    npm install -g @marp-team/marp-cli
    marp presentation.md --pptx -o output.pptx
    

进阶方案(适合有开发能力的用户)

如果你觉得 Markdown 生成的排版比较单一,想要套用企业专属的高级 PPT 模板:

你可以使用 Python 的 python-pptx 库写一个自动化脚本。

  1. 让本地大模型(通过 Ollama API)基于知识库输出纯 JSON 格式(包含:页码、标题、正文列表)。

  2. Python 脚本读取预先准备好的公司 .pptx 空白模板。

  3. 脚本解析 JSON,将标题和正文精准填入模板对应的占位符(Placeholder)中,最后保存。这种方式生成的 PPT 格式最完美,可以直接用于正式汇报。

硬件配置建议

纯离线运行 7B 到 14B 参数量的模型,并进行文本生成,你的电脑最好具备:

  • 内存:至少 16GB,推荐 32GB。

  • 显卡:NVIDIA RTX 3060/4060(8GB 显存可跑 7B 模型);RTX 4080/4090 或 Mac M芯片 (M2/M3 Max 32G统一内存及以上) 可流畅运行 14B-32B 模型,逻辑能力更强。

这是一个非常成熟且可落地的技术路线。为了给你提供更具体的实操建议,你目前手头的电脑硬件配置(特别是显存大小或是否是 Mac)大概是什么情况?另外,你更倾向于使用现成的可视化软件(零代码部署),还是打算自己写 Python 脚本把整个流程串联起来?

 

 

 

 

 

 

 

 

manus lite

纯离线环境下基于大模型与知识库的高质量 PPT 生成解决方案

在企业级应用和对数据隐私要求极高的场景中,纯离线环境下利用大语言模型(LLM)结合本地知识库(RAG)自动生成高质量的演示文稿(PPT)已成为一项重要需求。本文将为您提供一套完整的、经过验证的最佳实践解决方案,涵盖架构设计、工具选型以及详细的实施步骤。

核心架构设计

为了实现完全离线且高质量的 PPT 生成,系统架构需要分为三个核心层级:模型层、知识检索层(RAG)和应用生成层。这三个层级相互配合,确保数据不出本地网络,同时提供强大的内容生成和排版能力。
模型层负责提供自然语言理解和生成能力。在离线环境中,我们需要部署开源的大语言模型。知识检索层负责管理用户的私有数据,通过向量化技术将文档切片并存储,当用户提出需求时,检索出最相关的背景知识。应用生成层则是用户交互的入口,它负责接收用户指令,调用知识库获取上下文,引导大模型生成结构化的 PPT 大纲和内容,并最终渲染为标准的 PPTX 文件。

最佳工具选型

在众多开源工具中,经过对比分析,我们推荐以下技术栈组合作为最佳实践:

1. 模型层:Ollama + DeepSeek-R1 / Llama 3

Ollama 是目前最成熟、易用的本地大模型运行框架。它支持跨平台部署,并且对硬件资源的调度(尤其是 GPU 加速)优化得非常好。在模型选择上,推荐使用 DeepSeek-R1(如 8B 或 14B 版本,视本地显存而定)或 Llama 3 系列。DeepSeek-R1 在逻辑推理和中文理解方面表现优异,非常适合用于梳理复杂的知识库内容并生成逻辑严密的 PPT 大纲。

2. 知识检索层(RAG):Dify

Dify 是一款开源的 LLM 应用开发平台,内置了强大的 RAG 工作流引擎。它支持完全本地化部署,并且可以无缝接入 Ollama 提供的本地模型。通过 Dify,用户可以轻松上传 PDF、Word、TXT 等格式的私有文档,系统会自动进行文本分块(Chunking)和向量化存储。Dify 的可视化编排界面使得构建复杂的“检索-生成”工作流变得非常直观。

3. 应用生成层:Presenton

Presenton 是一个开源的 AI 演示文稿生成器,其最大的优势在于原生支持本地部署和 Ollama 集成。与许多依赖云端 API 的工具不同,Presenton 可以完全在离线环境中运行。它不仅支持通过提示词生成 PPT,还支持上传现有的 PPTX 文件,让 AI 学习用户的设计风格(品牌 DNA),从而生成符合企业视觉规范的高质量幻灯片。此外,它支持导出为可二次编辑的 PPTX 格式。

实施步骤指南

以下是基于上述技术栈的详细部署与实施流程。

第一步:环境准备与模型部署

首先,需要在本地服务器或高性能工作站上安装 Docker 环境,因为 Dify 和 Presenton 都推荐使用 Docker 进行容器化部署。为了保证生成速度,建议配备 NVIDIA 显卡并安装 NVIDIA Container Toolkit。
接着,安装 Ollama 并拉取所需的语言模型。通过命令行执行 ollama run deepseek-r1:8b 即可下载并启动模型。同时,为了支持文本向量化,还需要在 Ollama 中拉取一个 Embedding 模型,例如 nomic-embed-text

第二步:部署本地知识库系统

使用 Docker Compose 部署 Dify。在 Dify 的设置中,将模型供应商配置为本地的 Ollama 服务,分别接入之前下载的文本生成模型和 Embedding 模型。
配置完成后,在 Dify 中创建一个新的“知识库”,上传您的企业文档或参考资料。Dify 会自动处理这些文件并建立向量索引。随后,您可以创建一个“工作流应用”,配置其在接收到用户输入时,先从知识库中检索相关内容,然后将检索结果作为上下文发送给大模型,要求其输出结构化的 PPT 大纲。

第三步:部署与配置 PPT 生成引擎

通过 Docker 部署 Presenton。在启动容器时,通过环境变量指定使用 Ollama 作为 LLM 提供商,并配置相应的模型名称和 Ollama 服务地址。例如,设置 LLM="ollama"OLLAMA_MODEL="deepseek-r1:8b"
如果需要生成配图,Presenton 支持集成本地的 ComfyUI 或其他开源图像生成方案,从而实现完全离线的图文并茂。

第四步:工作流整合与实际应用

在实际使用中,用户可以通过 Dify 的接口输入主题(例如:“基于第一季度财务报表生成总结汇报”)。Dify 会检索财务报表数据,生成一份详细的 Markdown 格式大纲。
随后,将这份大纲输入到 Presenton 中。Presenton 会解析大纲结构,调用本地模型为每一页幻灯片扩写详细内容,并根据预设的模板(或通过学习企业现有 PPT 提取的品牌 DNA)进行排版渲染。最终,用户可以直接下载生成的 PPTX 文件进行最后的微调。

总结

通过整合 Ollama、Dify 和 Presenton,我们可以在完全断网的环境下,构建一套安全、高效且高度定制化的 AI PPT 生成系统。这套方案不仅保护了核心数据的隐私,还能通过 RAG 技术确保生成内容的准确性,并通过 Presenton 的模板学习功能保证了输出文档的专业美观。对于有严格保密要求的企业或机构而言,这是目前开源生态中最具可行性和落地价值的解决方案。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Dify 能否生成 PPT?

能,但有局限性。 Dify 本身不直接生成 PPT 文件,需要借助工具/插件实现。


Dify 生成 PPT 的常见方案

方案一:Dify + MARP / Slidev(Markdown转PPT)

text
用户输入主题 → Dify生成Markdown格式幻灯片内容 → MARP渲染为PPT/PDF

优点: 简单易用,格式规范
缺点: 样式单一,美观度有限

方案二:Dify + Python-pptx(代码生成)

text
Dify → 调用代码执行工具 → python-pptx生成.pptx文件
Python
# Dify工作流中调用的代码示例
from pptx import Presentation
from pptx.util import Inches, Pt

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[1])
title = slide.shapes.title
title.text = "AI生成的标题"
# ... 更多内容
prs.save('output.pptx')

优点: 可生成真正的PPT文件
缺点: 样式调整复杂,美观度差

方案三:Dify + 第三方PPT API

text
Dify工作流 → HTTP请求 → Beautiful.ai / Gamma API → 返回PPT

效果评价

维度评分说明
内容质量 ⭐⭐⭐⭐ AI生成内容逻辑清晰
视觉效果 ⭐⭐ 样式简陋,需手动美化
自动化程度 ⭐⭐⭐ 需要多工具组合
稳定性 ⭐⭐⭐ 依赖第三方工具
综合 ⭐⭐½ 够用但不惊艳

更好的解决方案推荐

🥇 方案一:Gamma.app(强烈推荐)

text
特点:
✅ AI一键生成完整PPT,设计精美
✅ 有API可集成到工作流
✅ 支持中文
✅ 效果远超Dify自生成

集成方式:Dify → HTTP节点 → Gamma API

🥈 方案二:MindShow

text
特点:
✅ 国产工具,中文友好
✅ 输入Markdown自动生成PPT
✅ 模板丰富美观
✅ 可通过API集成

流程:Dify生成MD内容 → MindShow渲染

🥉 方案三:美图AI PPT / 讯飞智文

text
特点:
✅ 国内服务,稳定
✅ 效果较好
✅ 有开放API

方案四:ChatPPT / AiPPT.cn

text
特点:
✅ 专注PPT生成
✅ 模板多样
✅ 支持API调用

推荐架构(最佳实践)

text
┌─────────────────────────────────────────┐
│           Dify 工作流                    │
│                                         │
│  用户输入主题/需求                       │
│       ↓                                 │
│  LLM节点:生成PPT大纲和内容结构          │
│       ↓                                 │
│  LLM节点:生成Markdown格式幻灯片         │
│       ↓                                 │
│  HTTP节点:调用Gamma/MindShow API        │
│       ↓                                 │
│  返回PPT链接/文件给用户                  │
└─────────────────────────────────────────┘

选型建议

场景推荐方案
追求美观,预算充足 Gamma API
国内使用,中文场景 讯飞智文 / MindShow
完全自主可控 Dify + python-pptx + 自定义模板
快速原型验证 Dify生成MD + MARP
企业级批量生成 Dify + 专业PPT API

总结

Dify 做 PPT 生成:内容生产是强项,视觉呈现是短板
最佳实践 = Dify(内容生成) + 专业PPT工具API(视觉渲染)
如果只用一个工具,直接用 Gamma.app 效果最好 🎯

posted on 2026-04-09 14:32  GKLBB  阅读(69)  评论(0)    收藏  举报