AI PPT与文档自动生成完全教程
从零掌握AI驱动的演示文稿与文档自动化生成技术,打造企业级报告自动化流水线
一、前言
在企业日常运营中,PPT演示文稿和文档报告是信息传递的核心载体。传统的文档制作流程——收集数据、撰写内容、设计排版、反复修改——往往耗费大量人力和时间。随着大语言模型(LLM)和AI生成技术的成熟,文档自动化生成已经从"模板填充"进化到"智能创作"阶段。
本教程将系统讲解AI PPT与文档自动生成的核心技术栈,涵盖从商业工具对比到程序化生成、从内容智能排版到完整流水线搭建的全链路方案。无论你是需要批量生成周报月报的开发者,还是希望为产品集成文档生成能力的工程师,都能从中找到实用的解决方案。
二、主流AI PPT工具全景对比
2.1 工具概览
目前市场上主流的AI PPT生成工具各有侧重:
| 工具 | 核心优势 | 适用场景 | API支持 | 价格 |
|---|---|---|---|---|
| Gamma | 输入一句话生成完整PPT,设计感强 | 快速原型、会议演示 | 有 | 免费版+Pro $10/月 |
| Beautiful.ai | 智能排版引擎,自动调整布局 | 商业汇报、销售提案 | 有 | $12/月起 |
| Tome | AI叙事驱动,适合讲故事 | 创意展示、产品发布 | 有 | 免费版+Pro $16/月 |
| SlidesGPT | 纯文本输入,快速生成 | 快速草稿、头脑风暴 | 有 | 按次付费 |
| Canva AI | 设计资源丰富,模板多样 | 营销材料、社交媒体 | 有限 | 免费版+Pro $13/月 |
2.2 Gamma API 集成示例
Gamma提供了REST API,可以通过代码调用AI生成演示文稿:
import requests
import json
import time
class GammaAPI:
"""Gamma AI PPT生成客户端"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.gamma.app/v1"
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def create_presentation(self, topic: str, num_slides: int = 10,
style: str = "professional") -> dict:
"""创建AI演示文稿"""
payload = {
"inputText": topic,
"numCards": num_slides,
"style": style,
"language": "zh-CN",
"format": "presentation"
}
response = requests.post(
f"{self.base_url}/generations",
headers=self.headers,
json=payload,
timeout=60
)
response.raise_for_status()
return response.json()
def poll_status(self, generation_id: str, max_wait: int = 300) -> dict:
"""轮询生成状态"""
start = time.time()
while time.time() - start < max_wait:
resp = requests.get(
f"{self.base_url}/generations/{generation_id}",
headers=self.headers
)
data = resp.json()
if data.get("status") == "completed":
return data
elif data.get("status") == "failed":
raise RuntimeError(f"生成失败: {data.get('error')}")
time.sleep(5)
raise TimeoutError("生成超时")
def export_pptx(self, generation_id: str, output_path: str):
"""导出为PPTX格式"""
resp = requests.get(
f"{self.base_url}/generations/{generation_id}/export/pptx",
headers=self.headers,
stream=True
)
resp.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
# 使用示例
# gamma = GammaAPI(api_key="your-api-key")
# result = gamma.create_presentation("2024年Q4销售业绩报告", num_slides=12)
# final = gamma.poll_status(result["id"])
# gamma.export_pptx(final["id"], "sales_report.pptx")
2.3 工具选择建议
- 快速原型验证:Gamma(一句话生成,速度最快)
- 企业级批量生成:python-pptx + LLM(完全可控,无API费用)
- 设计质量优先:Beautiful.ai(排版引擎最强)
- 叙事型内容:Tome(AI讲故事能力突出)
三、python-pptx 程序化PPT生成
对于需要完全控制输出格式、批量生成或与内部系统集成的场景,python-pptx 是最成熟的Python库。
3.1 环境安装与基础操作
pip install python-pptx
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
def create_basic_presentation():
"""创建基础演示文稿"""
prs = Presentation()
# 设置幻灯片尺寸(16:9)
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
# 使用空白布局
blank_layout = prs.slide_layouts[6]
# 添加标题页
slide = prs.slides.add_slide(blank_layout)
# 添加标题文本框
left = Inches(1.5)
top = Inches(2.5)
width = Inches(10)
height = Inches(1.5)
title_box = slide.shapes.add_textbox(left, top, width, height)
title_frame = title_box.text_frame
title_frame.word_wrap = True
p = title_frame.paragraphs[0]
p.text = "2024年第四季度销售业绩报告"
p.font.size = Pt(36)
p.font.bold = True
p.font.color.rgb = RGBColor(0x1A, 0x36, 0x5D)
p.alignment = PP_ALIGN.CENTER
# 添加副标题
subtitle_box = slide.shapes.add_textbox(
Inches(2), Inches(4.2), Inches(9), Inches(1)
)
subtitle_frame = subtitle_box.text_frame
p2 = subtitle_frame.paragraphs[0]
p2.text = "销售部 · 2024年12月"
p2.font.size = Pt(20)
p2.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
p2.alignment = PP_ALIGN.CENTER
prs.save("basic_report.pptx")
print("基础演示文稿已创建: basic_report.pptx")
create_basic_presentation()
3.2 高级版式与样式系统
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.oxml.ns import qn
import copy
class PresentationTheme:
"""演示文稿主题配置"""
def __init__(self):
# 颜色方案
self.primary_color = RGBColor(0x1A, 0x36, 0x5D) # 深蓝
self.secondary_color = RGBColor(0x2B, 0x57, 0x9A) # 中蓝
self.accent_color = RGBColor(0xE8, 0x6C, 0x00) # 橙色
self.text_color = RGBColor(0x33, 0x33, 0x33) # 深灰
self.light_text = RGBColor(0x99, 0x99, 0x99) # 浅灰
self.background = RGBColor(0xFF, 0xFF, 0xFF) # 白色
# 字体配置
self.title_font = "微软雅黑"
self.body_font = "宋体"
self.code_font = "Consolas"
# 尺寸配置
self.slide_width = Inches(13.333)
self.slide_height = Inches(7.5)
class SlideFactory:
"""幻灯片工厂 - 快速生成各类版式"""
def __init__(self, theme: PresentationTheme = None):
self.theme = theme or PresentationTheme()
self.prs = Presentation()
self.prs.slide_width = self.theme.slide_width
self.prs.slide_height = self.theme.slide_height
def _add_textbox(self, slide, left, top, width, height,
text, font_size=18, bold=False,
color=None, alignment=PP_ALIGN.LEFT,
font_name=None):
"""通用文本框添加方法"""
box = slide.shapes.add_textbox(Inches(left), Inches(top),
Inches(width), Inches(height))
frame = box.text_frame
frame.word_wrap = True
p = frame.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.color.rgb = color or self.theme.text_color
p.font.name = font_name or self.theme.body_font
p.alignment = alignment
return box, frame
def add_title_slide(self, title: str, subtitle: str = ""):
"""添加标题页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# 背景色块
bg = slide.shapes.add_shape(
1, # MSO_SHAPE.RECTANGLE
Inches(0), Inches(0),
self.theme.slide_width, self.theme.slide_height
)
bg.fill.solid()
bg.fill.fore_color.rgb = self.theme.primary_color
bg.line.fill.background()
# 标题
self._add_textbox(slide, 1.5, 2.2, 10, 1.5, title,
font_size=40, bold=True,
color=RGBColor(0xFF, 0xFF, 0xFF),
alignment=PP_ALIGN.CENTER,
font_name=self.theme.title_font)
# 副标题
if subtitle:
self._add_textbox(slide, 2, 4.0, 9, 1, subtitle,
font_size=22,
color=RGBColor(0xCC, 0xDD, 0xEE),
alignment=PP_ALIGN.CENTER)
return slide
def add_content_slide(self, title: str, bullet_points: list):
"""添加内容页(标题+要点列表)"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# 顶部色条
bar = slide.shapes.add_shape(
1, Inches(0), Inches(0),
self.theme.slide_width, Inches(0.08)
)
bar.fill.solid()
bar.fill.fore_color.rgb = self.theme.accent_color
bar.line.fill.background()
# 标题
self._add_textbox(slide, 0.8, 0.4, 11, 0.8, title,
font_size=28, bold=True,
color=self.theme.primary_color,
font_name=self.theme.title_font)
# 内容列表
content_box = slide.shapes.add_textbox(
Inches(1.0), Inches(1.6), Inches(11), Inches(5)
)
frame = content_box.text_frame
frame.word_wrap = True
for i, point in enumerate(bullet_points):
if i == 0:
p = frame.paragraphs[0]
else:
p = frame.add_paragraph()
p.text = f"• {point}"
p.font.size = Pt(18)
p.font.color.rgb = self.theme.text_color
p.font.name = self.theme.body_font
p.space_after = Pt(12)
return slide
def add_two_column_slide(self, title: str, left_content: list,
right_content: list):
"""添加双栏内容页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# 标题
self._add_textbox(slide, 0.8, 0.4, 11, 0.8, title,
font_size=28, bold=True,
color=self.theme.primary_color)
# 左栏
left_box = slide.shapes.add_textbox(
Inches(0.8), Inches(1.6), Inches(5.5), Inches(5)
)
left_frame = left_box.text_frame
left_frame.word_wrap = True
for i, item in enumerate(left_content):
p = left_frame.paragraphs[0] if i == 0 else left_frame.add_paragraph()
p.text = f"• {item}"
p.font.size = Pt(16)
p.space_after = Pt(10)
# 分隔线
line = slide.shapes.add_shape(
1, Inches(6.5), Inches(1.6),
Inches(0.02), Inches(4.5)
)
line.fill.solid()
line.fill.fore_color.rgb = RGBColor(0xDD, 0xDD, 0xDD)
line.line.fill.background()
# 右栏
right_box = slide.shapes.add_textbox(
Inches(7.0), Inches(1.6), Inches(5.5), Inches(5)
)
right_frame = right_box.text_frame
right_frame.word_wrap = True
for i, item in enumerate(right_content):
p = right_frame.paragraphs[0] if i == 0 else right_frame.add_paragraph()
p.text = f"• {item}"
p.font.size = Pt(16)
p.space_after = Pt(10)
return slide
def add_data_slide(self, title: str, chart_type: str,
categories: list, series_data: dict):
"""添加数据图表页"""
slide = self.prs.slides.add_slide(self.prs.slide_layouts[6])
# 标题
self._add_textbox(slide, 0.8, 0.4, 11, 0.8, title,
font_size=28, bold=True,
color=self.theme.primary_color)
# 图表数据
chart_data = CategoryChartData()
chart_data.categories = categories
for series_name, values in series_data.items():
chart_data.add_series(series_name, values)
# 图表类型映射
chart_types = {
'bar': XL_CHART_TYPE.BAR_CLUSTERED,
'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
'line': XL_CHART_TYPE.LINE,
'pie': XL_CHART_TYPE.PIE,
'area': XL_CHART_TYPE.AREA,
}
chart = slide.shapes.add_chart(
chart_types.get(chart_type, XL_CHART_TYPE.COLUMN_CLUSTERED),
Inches(1.0), Inches(1.8), Inches(11), Inches(5),
chart_data
).chart
chart.has_legend = True
chart.legend.include_in_layout = False
return slide
def save(self, path: str):
"""保存文件"""
self.prs.save(path)
print(f"演示文稿已保存: {path}")
# 使用示例
factory = SlideFactory()
factory.add_title_slide("AI技术年度总结", "技术部 · 2024年度")
factory.add_content_slide("核心成果", [
"完成大语言模型平台搭建,支撑10+业务线",
"AI助手日活用户突破5万",
"自动化报告系统节省人工80%",
"模型推理成本降低60%"
])
factory.add_two_column_slide(
"技术栈对比",
["PyTorch 2.0 — 训练效率提升40%", "vLLM — 推理吞吐量提升3x", "LangChain — RAG准确率92%"],
["TensorRT — 延迟降低50%", "ONNX Runtime — 跨平台部署", "Milvus — 向量检索P99<10ms"]
)
factory.add_data_slide("季度营收趋势", "column",
["Q1", "Q2", "Q3", "Q4"],
{"2023": [1200, 1500, 1800, 2100], "2024": [1800, 2200, 2600, 3200]}
)
factory.save("ai_annual_report.pptx")
四、LLM驱动的内容结构化与大纲生成
4.1 基于LLM的PPT大纲自动生成
利用大语言模型将用户输入的主题自动扩展为结构化的PPT大纲:
import json
from openai import OpenAI
class PPTOutlineGenerator:
"""基于LLM的PPT大纲生成器"""
def __init__(self, api_key: str, base_url: str = None, model: str = "gpt-4"):
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.model = model
def generate_outline(self, topic: str, num_slides: int = 10,
style: str = "professional",
audience: str = "企业管理层") -> dict:
"""生成PPT大纲"""
system_prompt = """你是一个专业的演示文稿大纲生成专家。请根据用户输入的主题,
生成结构化的PPT大纲,输出为JSON格式。
输出格式要求:
{
"title": "演示文稿主标题",
"subtitle": "副标题",
"slides": [
{
"slide_number": 1,
"type": "title|content|two_column|data|summary",
"title": "幻灯片标题",
"content": ["要点1", "要点2", ...],
"left_content": ["左栏内容"], // 仅two_column类型
"right_content": ["右栏内容"], // 仅two_column类型
"chart_hint": "图表类型和数据建议", // 仅data类型
"speaker_notes": "演讲者备注"
}
]
}"""
user_prompt = f"""请为以下主题生成PPT大纲:
- 主题:{topic}
- 幻灯片数量:{num_slides}页
- 演示风格:{style}
- 目标受众:{audience}
要求:
1. 开头要有引人入胜的开场
2. 中间部分逻辑清晰、层层递进
3. 包含数据支撑的页面
4. 结尾有总结和行动建议
5. 每页内容精炼,不超过5个要点"""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.7,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
def expand_bullet_points(self, topic: str, bullet_points: list,
detail_level: str = "medium") -> list:
"""扩展要点为详细内容"""
prompt = f"""请将以下PPT要点扩展为详细的讲解内容(每条100-200字):
主题:{topic}
要点:
{json.dumps(bullet_points, ensure_ascii=False, indent=2)}
要求:
- 语言专业但不晦涩
- 包含具体数据或案例
- 适合口头讲解
- 详细程度:{detail_level}
输出为JSON数组,每个元素包含原始要点和扩展内容。"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
# 使用示例
# generator = PPTOutlineGenerator(api_key="your-key")
# outline = generator.generate_outline(
# "2024年AI技术趋势与企业应用",
# num_slides=12,
# style="professional",
# audience="CTO和VP级别管理者"
# )
# print(json.dumps(outline, ensure_ascii=False, indent=2))
4.2 智能内容提取与结构化
从业务数据、会议记录、研究报告等非结构化文本中提取PPT所需内容:
import json
from dataclasses import dataclass, field, asdict
from typing import Optional
@dataclass
class SlideContent:
"""幻灯片内容数据结构"""
title: str
slide_type: str = "content"
bullet_points: list = field(default_factory=list)
key_data: dict = field(default_factory=dict)
speaker_notes: str = ""
priority: int = 0 # 用于排序
class ContentExtractor:
"""从非结构化文本提取PPT内容"""
def __init__(self, llm_client, model: str = "gpt-4"):
self.client = llm_client
self.model = model
def extract_from_report(self, text: str, max_slides: int = 10) -> list:
"""从报告文本提取PPT内容"""
prompt = f"""请从以下报告文本中提取适合制作PPT的核心内容,
最多提取{max_slides}个关键点:
{text[:5000]} # 截断避免token超限
输出JSON格式:
{{
"slides": [
{{
"title": "幻灯片标题",
"slide_type": "content/data/two_column",
"bullet_points": ["要点1", "要点2"],
"key_data": {{"metric_name": "value"}},
"priority": 1
}}
]
}}
提取原则:
1. 优先提取有数据支撑的内容
2. 按重要性排序
3. 每个幻灯片不超过5个要点
4. 识别适合用图表展示的数据"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
return [SlideContent(**item) for item in result.get("slides", [])]
def extract_from_meeting_notes(self, notes: str) -> dict:
"""从会议纪要提取行动项和决策"""
prompt = f"""请从以下会议纪要中提取:
1. 关键决策(decisions)
2. 行动项(action_items)含负责人和截止日期
3. 待解决问题(open_questions)
会议纪要:
{notes[:3000]}
输出JSON格式。"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
五、图表与数据可视化自动嵌入
5.1 基于matplotlib的图表自动生成
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg') # 无GUI模式
import numpy as np
from io import BytesIO
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
class ChartGenerator:
"""图表生成器 - 自动生成PPT所需的图表图片"""
def __init__(self, style: str = "seaborn-v0_8-whitegrid"):
try:
plt.style.use(style)
except OSError:
plt.style.use('default')
self.color_palette = [
'#1A365D', '#2B579A', '#E86C00', '#28A745',
'#DC3545', '#6F42C1', '#17A2B8', '#FFC107'
]
def bar_chart(self, categories: list, values: list,
title: str = "", ylabel: str = "") -> BytesIO:
"""生成柱状图"""
fig, ax = plt.subplots(figsize=(10, 6))
bars = ax.bar(categories, values, color=self.color_palette[:len(categories)],
width=0.6, edgecolor='white', linewidth=0.5)
# 在柱子上方显示数值
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
f'{val:,.0f}', ha='center', va='bottom', fontsize=11)
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
ax.set_ylabel(ylabel, fontsize=12)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
buf = BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
plt.close(fig)
return buf
def line_chart(self, x_data: list, series: dict,
title: str = "", ylabel: str = "") -> BytesIO:
"""生成折线图"""
fig, ax = plt.subplots(figsize=(10, 6))
for i, (name, values) in enumerate(series.items()):
color = self.color_palette[i % len(self.color_palette)]
ax.plot(x_data, values, marker='o', label=name,
color=color, linewidth=2, markersize=6)
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
ax.set_ylabel(ylabel, fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
buf = BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
plt.close(fig)
return buf
def pie_chart(self, labels: list, values: list,
title: str = "") -> BytesIO:
"""生成饼图"""
fig, ax = plt.subplots(figsize=(8, 8))
colors = self.color_palette[:len(labels)]
wedges, texts, autotexts = ax.pie(
values, labels=labels, colors=colors,
autopct='%1.1f%%', startangle=90,
textprops={'fontsize': 12}
)
for autotext in autotexts:
autotext.set_fontsize(11)
autotext.set_fontweight('bold')
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
plt.tight_layout()
buf = BytesIO()
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
buf.seek(0)
plt.close(fig)
return buf
def add_chart_to_slide(self, slide, chart_buf: BytesIO,
left: float = 1.0, top: float = 1.8,
width: float = 10, height: float = 5):
"""将图表插入幻灯片"""
from pptx.util import Inches
slide.shapes.add_picture(
chart_buf, Inches(left), Inches(top),
Inches(width), Inches(height)
)
# 使用示例
chart_gen = ChartGenerator()
# 生成柱状图
buf = chart_gen.bar_chart(
['北京', '上海', '广州', '深圳', '杭州'],
[3200, 2800, 1900, 2100, 1500],
title='各区域Q4销售额(万元)',
ylabel='销售额(万元)'
)
# 生成折线图
buf2 = chart_gen.line_chart(
['1月', '2月', '3月', '4月', '5月', '6月'],
{'2023': [100, 120, 135, 148, 160, 175], '2024': [130, 155, 170, 195, 210, 240]},
title='月度用户增长趋势',
ylabel='用户数(万)'
)
5.2 动态数据绑定
import pandas as pd
from datetime import datetime
class DataDrivenSlideBuilder:
"""数据驱动的幻灯片构建器"""
def __init__(self, factory: SlideFactory, chart_gen: ChartGenerator):
self.factory = factory
self.chart_gen = chart_gen
def build_from_dataframe(self, df: pd.DataFrame, title: str,
chart_type: str = "column"):
"""从DataFrame自动生成数据幻灯片"""
# 自动识别数据列类型
numeric_cols = df.select_dtypes(include=['number']).columns.tolist()
category_cols = df.select_dtypes(include=['object', 'category']).columns.tolist()
if not category_cols or not numeric_cols:
raise ValueError("DataFrame需包含至少一个分类列和一个数值列")
# 构建图表数据
categories = df[category_cols[0]].tolist()
series_data = {}
for col in numeric_cols[:4]: # 最多4个系列
series_data[col] = df[col].tolist()
# 添加数据幻灯片
self.factory.add_data_slide(title, chart_type, categories, series_data)
def build_summary_slide(self, metrics: dict, title: str = "关键指标总览"):
"""构建指标汇总页"""
slide = self.factory.prs.slides.add_slide(
self.factory.prs.slide_layouts[6]
)
# 标题
self.factory._add_textbox(slide, 0.8, 0.4, 11, 0.8, title,
font_size=28, bold=True,
color=self.factory.theme.primary_color)
# 指标卡片网格(2行3列)
positions = [
(0.8, 1.8), (4.8, 1.8), (8.8, 1.8),
(0.8, 4.2), (4.8, 4.2), (8.8, 4.2),
]
for i, (name, value) in enumerate(list(metrics.items())[:6]):
if i >= len(positions):
break
left, top = positions[i]
# 卡片背景
card = slide.shapes.add_shape(
1, Inches(left), Inches(top),
Inches(3.5), Inches(2)
)
card.fill.solid()
card.fill.fore_color.rgb = RGBColor(0xF5, 0xF7, 0xFA)
card.line.fill.background()
# 指标名称
self.factory._add_textbox(slide, left + 0.3, top + 0.3,
3, 0.5, name,
font_size=14,
color=self.factory.theme.light_text)
# 指标值
self.factory._add_textbox(slide, left + 0.3, top + 0.9,
3, 0.8, str(value),
font_size=32, bold=True,
color=self.factory.theme.primary_color)
六、多格式导出系统
6.1 PPT转PDF
import subprocess
import os
class DocumentExporter:
"""多格式文档导出器"""
@staticmethod
def pptx_to_pdf_libreoffice(input_path: str, output_dir: str = None):
"""使用LibreOffice将PPTX转换为PDF"""
if output_dir is None:
output_dir = os.path.dirname(input_path)
cmd = [
'libreoffice', '--headless', '--convert-to', 'pdf',
'--outdir', output_dir, input_path
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode != 0:
raise RuntimeError(f"转换失败: {result.stderr}")
pdf_name = os.path.splitext(os.path.basename(input_path))[0] + '.pdf'
return os.path.join(output_dir, pdf_name)
@staticmethod
def pptx_to_html(input_path: str, output_path: str):
"""将PPTX转换为HTML(简化版)"""
from pptx import Presentation
prs = Presentation(input_path)
html_parts = ['''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Presentation</title>
<style>
body { font-family: 'Microsoft YaHei', sans-serif; margin: 0; background: #f0f0f0; }
.slide { width: 960px; height: 540px; margin: 20px auto; background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 40px;
box-sizing: border-box; position: relative; overflow: hidden; }
.slide-number { position: absolute; bottom: 10px; right: 20px; color: #999; font-size: 12px; }
h1 { color: #1A365D; font-size: 28px; }
p { color: #333; font-size: 16px; line-height: 1.8; }
</style>
</head>
<body>''']
for i, slide in enumerate(prs.slides, 1):
html_parts.append(f'<div class="slide" id="slide-{i}">')
for shape in slide.shapes:
if shape.has_text_frame:
for para in shape.text_frame.paragraphs:
text = para.text.strip()
if text:
if any(run.font.bold for run in para.runs if run.font.bold):
html_parts.append(f' <h1>{text}</h1>')
else:
html_parts.append(f' <p>{text}</p>')
elif hasattr(shape, 'image'):
import base64
img_data = base64.b64encode(shape.image.blob).decode()
ext = shape.image.content_type.split('/')[-1]
html_parts.append(
f' <img src="data:image/{ext};base64,{img_data}" '
f'style="max-width:100%;max-height:400px;">'
)
html_parts.append(f' <div class="slide-number">{i}</div>')
html_parts.append('</div>')
html_parts.append('</body></html>')
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(html_parts))
return output_path
@staticmethod
def generate_thumbnail(input_path: str, output_path: str,
slide_number: int = 0):
"""生成幻灯片缩略图(需要pdf2image)"""
from pdf2image import convert_from_path
# 先转PDF
pdf_path = DocumentExporter.pptx_to_pdf_libreoffice(input_path)
# PDF转图片
images = convert_from_path(pdf_path, first_page=slide_number+1,
last_page=slide_number+1, dpi=150)
if images:
images[0].save(output_path, 'PNG')
return output_path
return None
七、Word文档AI生成
7.1 python-docx程序化生成
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
class ReportGenerator:
"""Word报告生成器"""
def __init__(self, title: str, author: str = ""):
self.doc = Document()
self.title = title
self.author = author
self._setup_styles()
def _setup_styles(self):
"""设置文档样式"""
# 设置默认字体
style = self.doc.styles['Normal']
style.font.name = '宋体'
style.font.size = Pt(12)
style.paragraph_format.line_spacing = 1.5
# 设置标题样式
for level in range(1, 4):
heading_style = self.doc.styles[f'Heading {level}']
heading_style.font.name = '微软雅黑'
heading_style.font.color.rgb = RGBColor(0x1A, 0x36, 0x5D)
def add_cover_page(self, date: str = None):
"""添加封面页"""
if date is None:
from datetime import datetime
date = datetime.now().strftime("%Y年%m月")
# 空行
for _ in range(6):
self.doc.add_paragraph()
# 标题
title_para = self.doc.add_paragraph()
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = title_para.add_run(self.title)
run.font.size = Pt(36)
run.font.bold = True
run.font.color.rgb = RGBColor(0x1A, 0x36, 0x5D)
# 日期
date_para = self.doc.add_paragraph()
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = date_para.add_run(date)
run.font.size = Pt(16)
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
if self.author:
author_para = self.doc.add_paragraph()
author_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = author_para.add_run(self.author)
run.font.size = Pt(14)
self.doc.add_page_break()
def add_table(self, headers: list, rows: list, title: str = None):
"""添加表格"""
if title:
self.doc.add_heading(title, level=2)
table = self.doc.add_table(rows=len(rows)+1, cols=len(headers))
table.style = 'Light Grid Accent 1'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
# 表头
for i, header in enumerate(headers):
cell = table.rows[0].cells[i]
cell.text = header
for paragraph in cell.paragraphs:
for run in paragraph.runs:
run.font.bold = True
# 数据行
for row_idx, row_data in enumerate(rows, 1):
for col_idx, value in enumerate(row_data):
table.rows[row_idx].cells[col_idx].text = str(value)
self.doc.add_paragraph() # 表格后空行
def add_chart_image(self, image_path: str, width: float = 6.0,
caption: str = ""):
"""添加图表图片"""
self.doc.add_picture(image_path, width=Inches(width))
if caption:
cap_para = self.doc.add_paragraph()
cap_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = cap_para.add_run(caption)
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0x99, 0x99, 0x99)
def save(self, path: str):
"""保存文档"""
self.doc.save(path)
print(f"报告已保存: {path}")
# 使用示例
report = ReportGenerator("2024年第四季度销售业绩报告", "销售部")
report.add_cover_page("2024年12月")
report.doc.add_heading("一、执行摘要", level=1)
report.doc.add_paragraph(
"本季度销售额达到3200万元,同比增长35.8%。其中华东区域表现最为突出,"
"贡献了总销售额的42%。新客户获取成本较上季度降低18%,客户留存率提升至92.5%。"
)
report.add_table(
headers=["区域", "Q3销售额", "Q4销售额", "环比增长"],
rows=[
["华东", "1,200万", "1,344万", "+12%"],
["华南", "800万", "880万", "+10%"],
["华北", "600万", "720万", "+20%"],
["西部", "400万", "256万", "-36%"],
],
title="各区域业绩对比"
)
report.save("Q4_sales_report.docx")
八、完整案例:季度报告自动化生成系统
8.1 系统架构
"""
季度报告自动化生成系统
整合LLM内容生成、图表生成、PPT/Word导出的完整流水线
"""
import json
import os
from datetime import datetime
from dataclasses import dataclass, field
@dataclass
class ReportConfig:
"""报告配置"""
company_name: str
report_period: str
department: str
template_path: str = None
output_dir: str = "./output"
include_pptx: bool = True
include_docx: bool = True
include_pdf: bool = True
include_html: bool = True
theme_colors: dict = field(default_factory=lambda: {
'primary': '#1A365D',
'secondary': '#2B579A',
'accent': '#E86C00'
})
class QuarterlyReportPipeline:
"""季度报告自动化流水线"""
def __init__(self, config: ReportConfig, llm_api_key: str,
llm_base_url: str = None):
self.config = config
self.outline_gen = PPTOutlineGenerator(llm_api_key, llm_base_url)
self.chart_gen = ChartGenerator()
self.exporter = DocumentExporter()
os.makedirs(config.output_dir, exist_ok=True)
def run(self, data_source: dict, supplementary_text: str = ""):
"""执行完整报告生成流水线"""
print(f"{'='*50}")
print(f"开始生成 {self.config.report_period} 季度报告")
print(f"{'='*50}")
# Step 1: LLM生成大纲
print("\n[1/6] 生成报告大纲...")
outline = self.outline_gen.generate_outline(
f"{self.config.company_name} {self.config.report_period} "
f"{self.config.department}季度报告",
num_slides=12,
style="professional"
)
# Step 2: 生成图表
print("[2/6] 生成数据图表...")
charts = self._generate_charts(data_source)
# Step 3: 构建PPT
if self.config.include_pptx:
print("[3/6] 构建PPT演示文稿...")
pptx_path = self._build_pptx(outline, charts, data_source)
else:
pptx_path = None
# Step 4: 构建Word报告
if self.config.include_docx:
print("[4/6] 构建Word报告...")
docx_path = self._build_docx(outline, charts, data_source)
else:
docx_path = None
# Step 5: 转换格式
print("[5/6] 导出多格式文件...")
outputs = {'pptx': pptx_path, 'docx': docx_path}
if pptx_path and self.config.include_pdf:
try:
pdf_path = self.exporter.pptx_to_pdf_libreoffice(pptx_path)
outputs['pdf'] = pdf_path
print(f" ✓ PDF: {pdf_path}")
except Exception as e:
print(f" ✗ PDF转换失败: {e}")
if pptx_path and self.config.include_html:
html_path = os.path.join(self.config.output_dir, 'report.html')
self.exporter.pptx_to_html(pptx_path, html_path)
outputs['html'] = html_path
print(f" ✓ HTML: {html_path}")
# Step 6: 生成摘要
print("[6/6] 生成执行摘要...")
summary = self._generate_summary(outline, data_source)
print(f"\n{'='*50}")
print("报告生成完成!")
for fmt, path in outputs.items():
if path:
print(f" {fmt.upper()}: {path}")
print(f"{'='*50}")
return outputs
def _generate_charts(self, data: dict) -> dict:
"""生成所有图表"""
charts = {}
# 营收趋势图
if 'revenue' in data:
charts['revenue_trend'] = self.chart_gen.line_chart(
data['revenue']['months'],
data['revenue']['series'],
title='月度营收趋势',
ylabel='营收(万元)'
)
# 区域分布饼图
if 'regional' in data:
charts['regional_dist'] = self.chart_gen.pie_chart(
data['regional']['labels'],
data['regional']['values'],
title='区域销售分布'
)
# 产品对比柱状图
if 'products' in data:
charts['product_compare'] = self.chart_gen.bar_chart(
data['products']['names'],
data['products']['sales'],
title='产品线销售对比',
ylabel='销售额(万元)'
)
return charts
def _build_pptx(self, outline: dict, charts: dict, data: dict) -> str:
"""构建PPT文件"""
theme = PresentationTheme()
factory = SlideFactory(theme)
# 封面
factory.add_title_slide(
outline.get('title', f'{self.config.report_period}季度报告'),
outline.get('subtitle', f'{self.config.department} · {self.config.company_name}')
)
# 内容页
for slide_data in outline.get('slides', []):
slide_type = slide_data.get('type', 'content')
if slide_type == 'content':
factory.add_content_slide(
slide_data['title'],
slide_data.get('content', [])
)
elif slide_type == 'two_column':
factory.add_two_column_slide(
slide_data['title'],
slide_data.get('left_content', []),
slide_data.get('right_content', [])
)
elif slide_type == 'data':
# 尝试嵌入图表
chart_key = slide_data.get('chart_hint', '')
if chart_key in charts:
slide = factory.prs.slides.add_slide(
factory.prs.slide_layouts[6]
)
factory._add_textbox(slide, 0.8, 0.4, 11, 0.8,
slide_data['title'], font_size=28,
bold=True, color=theme.primary_color)
factory.chart_gen = self.chart_gen
self.chart_gen.add_chart_to_slide(slide, charts[chart_key])
# 结尾页
factory.add_title_slide("谢谢", f"Q&A · {self.config.company_name}")
path = os.path.join(self.config.output_dir,
f'{self.config.report_period}_report.pptx')
factory.save(path)
return path
def _build_docx(self, outline: dict, charts: dict, data: dict) -> str:
"""构建Word文档"""
report = ReportGenerator(
outline.get('title', f'{self.config.report_period}季度报告'),
author=self.config.department
)
report.add_cover_page(self.config.report_period)
# 目录占位
report.doc.add_heading("目录", level=1)
report.doc.add_paragraph("(请在Word中右键更新目录域)")
report.doc.add_page_break()
# 内容
for i, slide_data in enumerate(outline.get('slides', []), 1):
report.doc.add_heading(f"{i}. {slide_data['title']}", level=1)
for point in slide_data.get('content', []):
report.doc.add_paragraph(point, style='List Bullet')
# 添加相关图表
chart_key = slide_data.get('chart_hint', '')
if chart_key and chart_key in charts:
chart_path = os.path.join(self.config.output_dir,
f'chart_{chart_key}.png')
charts[chart_key].seek(0)
with open(chart_path, 'wb') as f:
f.write(charts[chart_key].read())
charts[chart_key].seek(0)
report.add_chart_image(chart_path, caption=slide_data['title'])
path = os.path.join(self.config.output_dir,
f'{self.config.report_period}_report.docx')
report.save(path)
return path
def _generate_summary(self, outline: dict, data: dict) -> str:
"""生成执行摘要"""
# 简化版:从大纲中提取关键信息
key_points = []
for slide in outline.get('slides', []):
if slide.get('priority', 0) <= 3:
key_points.extend(slide.get('content', [])[:2])
summary = "执行摘要:\n\n"
for i, point in enumerate(key_points[:5], 1):
summary += f"{i}. {point}\n"
summary_path = os.path.join(self.config.output_dir, 'summary.txt')
with open(summary_path, 'w', encoding='utf-8') as f:
f.write(summary)
return summary
# ========== 使用示例 ==========
def demo_quarterly_report():
"""演示:生成季度报告"""
# 配置
config = ReportConfig(
company_name="智远科技",
report_period="2024-Q4",
department="销售部",
output_dir="./quarterly_report_output",
include_pptx=True,
include_docx=True,
include_pdf=True,
include_html=True
)
# 模拟业务数据
data = {
'revenue': {
'months': ['10月', '11月', '12月'],
'series': {
'2023': [950, 1050, 1200],
'2024': [1200, 1350, 1650]
}
},
'regional': {
'labels': ['华东', '华南', '华北', '西部', '海外'],
'values': [35, 25, 20, 12, 8]
},
'products': {
'names': ['AI平台', '数据服务', '智能硬件', '咨询'],
'sales': [1800, 650, 480, 270]
}
}
# 运行流水线(需要有效的API Key)
# pipeline = QuarterlyReportPipeline(
# config,
# llm_api_key="your-api-key",
# llm_base_url="https://api.openai.com/v1"
# )
# outputs = pipeline.run(data)
print("季度报告自动化生成系统已配置完成")
print(f"输出目录: {config.output_dir}")
print(f"生成格式: PPTX={config.include_pptx}, DOCX={config.include_docx}, "
f"PDF={config.include_pdf}, HTML={config.include_html}")
demo_quarterly_report()
九、API集成与批量生成
9.1 REST API封装
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing import Optional
import uuid
app = FastAPI(title="AI文档生成服务")
class GenerateRequest(BaseModel):
topic: str
num_slides: int = 10
style: str = "professional"
format: list = ["pptx"] # pptx, docx, pdf, html
data: Optional[dict] = None
class GenerateResponse(BaseModel):
task_id: str
status: str
message: str
# 任务存储
tasks = {}
def generate_task(task_id: str, request: GenerateRequest):
"""后台生成任务"""
try:
tasks[task_id]['status'] = 'processing'
config = ReportConfig(
company_name="自动生成",
report_period=request.topic,
department="",
output_dir=f"./output/{task_id}",
include_pptx='pptx' in request.format,
include_docx='docx' in request.format,
include_pdf='pdf' in request.format,
include_html='html' in request.format
)
# pipeline = QuarterlyReportPipeline(config, os.environ['LLM_API_KEY'])
# outputs = pipeline.run(request.data or {})
tasks[task_id]['status'] = 'completed'
tasks[task_id]['files'] = {'pptx': f'./output/{task_id}/report.pptx'}
except Exception as e:
tasks[task_id]['status'] = 'failed'
tasks[task_id]['error'] = str(e)
@app.post("/api/generate", response_model=GenerateResponse)
async def generate_report(request: GenerateRequest,
background_tasks: BackgroundTasks):
"""提交报告生成任务"""
task_id = str(uuid.uuid4())[:8]
tasks[task_id] = {'status': 'queued'}
background_tasks.add_task(generate_task, task_id, request)
return GenerateResponse(
task_id=task_id,
status='queued',
message='报告生成任务已提交'
)
@app.get("/api/task/{task_id}")
async def get_task_status(task_id: str):
"""查询任务状态"""
if task_id not in tasks:
raise HTTPException(status_code=404, detail="任务不存在")
return tasks[task_id]
# 启动命令: uvicorn main:app --host 0.0.0.0 --port 8000
9.2 批量生成脚本
import csv
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
class BatchGenerator:
"""批量报告生成器"""
def __init__(self, pipeline_factory, max_workers: int = 4):
self.pipeline_factory = pipeline_factory
self.max_workers = max_workers
def generate_from_csv(self, csv_path: str, template_config: dict):
"""从CSV批量生成报告"""
with open(csv_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
tasks = list(reader)
print(f"读取到 {len(tasks)} 个生成任务")
results = []
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = {}
for i, row in enumerate(tasks):
config = ReportConfig(
company_name=row.get('company', '未命名'),
report_period=row.get('period', f'Task-{i}'),
department=row.get('department', ''),
output_dir=f"./batch_output/{i}",
**template_config
)
pipeline = self.pipeline_factory(config)
future = executor.submit(pipeline.run, json.loads(row.get('data', '{}')))
futures[future] = row
for future in as_completed(futures):
row = futures[future]
try:
outputs = future.result()
results.append({
'period': row.get('period'),
'status': 'success',
'files': outputs
})
print(f" ✓ {row.get('period')} 完成")
except Exception as e:
results.append({
'period': row.get('period'),
'status': 'failed',
'error': str(e)
})
print(f" ✗ {row.get('period')} 失败: {e}")
return results
# 使用示例CSV格式:
# company,period,department,data
# 智远科技,2024-Q1,销售部,"{""revenue"": {...}}"
# 智远科技,2024-Q2,销售部,"{""revenue"": {...}}"
# 智远科技,2024-Q3,销售部,"{""revenue"": {...}}"
十、最佳实践与常见问题
10.1 性能优化建议
- 缓存LLM结果:相同主题的大纲生成结果应缓存,避免重复调用API
- 并行图表生成:多个图表可以使用ThreadPoolExecutor并行生成
- 模板预加载:将常用的PPT模板预加载为Presentation对象,避免重复解析
- 图片压缩:插入PPT前对图片进行压缩,减小文件体积
10.2 常见问题
Q: 中文字体在不同系统上显示不一致? A: 在代码中指定通用字体(如"微软雅黑"、"宋体"),或在PPT中嵌入字体:
# python-pptx暂不支持字体嵌入,需在模板中预设
# 或使用LibreOffice转换时指定字体
Q: 生成的图表中文显示为方块? A: 确保系统安装了中文字体,并正确设置matplotlib:
import matplotlib.font_manager as fm
# 查看可用字体
for font in fm.fontManager.ttflist:
if 'Hei' in font.name or 'Song' in font.name:
print(font.name, font.fname)
Q: LibreOffice转换PDF失败? A: 确保安装了LibreOffice:
# Ubuntu/Debian
sudo apt-get install libreoffice-impress
# CentOS/RHEL
sudo yum install libreoffice-impress
Q: 批量生成时API限流? A: 实现指数退避重试:
import time
def call_with_retry(func, max_retries=3, base_delay=1):
for attempt in range(max_retries):
try:
return func()
except Exception as e:
if 'rate_limit' in str(e).lower() and attempt < max_retries - 1:
delay = base_delay * (2 ** attempt)
print(f"限流,{delay}秒后重试...")
time.sleep(delay)
else:
raise
十一、总结
AI PPT与文档自动生成技术已经从概念验证走向生产可用。核心要点:
- 工具选择:快速原型用Gamma,企业级批量生成用python-pptx + LLM
- 内容结构化:LLM负责内容生成和结构化,代码负责排版和渲染
- 模板系统:建立统一的主题/样式系统,确保输出一致性
- 多格式输出:PPTX + PDF + HTML + Word满足不同分发场景
- 流水线化:将数据获取→内容生成→图表制作→文档组装串联为自动化流水线
- API化服务:封装为REST API支持按需调用和批量任务
掌握这些技术,你可以为团队构建一个"输入数据,输出报告"的全自动文档工厂,将原本数小时的文档制作工作压缩到分钟级别。
本教程配套完整代码已开源,欢迎Star和PR。