缘起:一个基金经理的吐槽
先聊聊为什么做这个项目。
我的一个朋友是基金经理,有天他跟我抱怨:"我脑子里有完整的投资逻辑,手里有详实的 Excel 数据,手机里还有刚开完的会议录音。但要把这些变成一份能上投决会的 PPT,我得花整整两天。"
作为开发者,我的第一反应是:这不就是一个 多模态输入 → 结构化输出 的问题吗?
然后我就开始做了。几个月后的今天,这个项目在金融、咨询等行业实际跑了一段时间,踩了无数坑,现在决定开源出来。
先看效果:
GitHub 地址在文末,先聊聊技术实现。
整体架构
┌─────────────────────────────────────────────────┐
│ Frontend (React 18) │
│ 输入面板 ← 对话式交互 → 预览面板(实时渲染) │
└──────────────────────┬──────────────────────────┘
│ HTTP/REST
┌──────────────────────▼──────────────────────────┐
│ Backend (FastAPI 异步架构) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ ASR 模块 │ │ 文本推理 │ │ 图片生成模块 │ │
│ │ (讯飞API) │ │(Gemini) │ │ (Gemini) │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 文档抽取 │ │ 母版分析 │ │ 素材嵌入引擎 │ │
│ │(PDF/Word │ │(Style │ │ (图片+表格) │ │
│ │ /Excel) │ │ Transfer)│ │ │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │
│ Session Manager → 状态机驱动的多轮对话流程 │
└─────────────────────────────────────────────────┘
技术栈:
- 前端:React 18,单文件 App.js(约 2300 行),纯函数组件 + Hooks
- 后端:FastAPI 异步架构,模块化设计(9 个模块 + 主入口共约 2800 行)
- AI:Google Gemini API(
gemini-3-pro-preview文本推理 +gemini-3-pro-image-preview图片生成) - 语音:科大讯飞非实时转写 API(支持说话人分离)
- 文档处理:PyMuPDF + python-docx + python-pptx + openpyxl
看起来不复杂,但魔鬼在细节里。下面聊聊几个核心技术难点。
难点一:多轮对话的状态管理
PPT 生成不是一个一次性的任务,而是一个多阶段、可回退的工作流:
输入想法 → 生成大纲 → 迭代大纲 → 生成设计方案 → 迭代设计 → 逐页生成图片 → 单页微调
每个阶段都支持用户用自然语言修改,比如"第3页加个风险提示"、"配色再稳重一些"。
我用了一个状态机来管理整个流程:
# modules/session.py
class SessionStage:
INPUT = "input" # 用户输入想法
OUTLINE = "outline" # 生成大纲
OUTLINE_REFINE = "outline_refine" # 大纲迭代
STYLE = "style" # 生成设计风格
STYLE_REFINE = "style_refine" # 风格迭代
GENERATE = "generate" # 生成图片
COMPLETE = "complete" # 完成
每个 Session 维护完整的上下文,数据结构如下:
def get_session(session_id: str) -> dict:
if session_id not in sessions:
sessions[session_id] = {
"stage": SessionStage.INPUT,
"user_input": "",
"outline_text": "", "outline_json": [],
"style_text": "", "style_json": [],
"generated_images": [],
"reference_image_path": None,
# 用户设置
"page_count": None, # 页数限制
"page_instructions": "", # 逐页说明
"design_principles": DEFAULT_DESIGN_PRINCIPLES,
# 多模态输入
"audio_transcript": "", # 录音转写文本
"support_docs_text": "", # 支持性文档文本
"support_docs_files": [], # 文档列表
"page_materials": {}, # 页面素材 {page_index: [材料列表]}
}
return sessions[session_id]
关键设计:有一个统一的 /api/chat 端点,根据当前状态自动路由到对应阶段的处理逻辑,前端不需要关心当前处于哪个阶段:
# server.py - 统一对话入口
@app.post("/api/chat")
async def chat(request: UserInputRequest):
session = get_session(request.session_id)
stage = session["stage"]
if stage == SessionStage.INPUT:
return await generate_outline(request)
elif stage in [SessionStage.OUTLINE, SessionStage.OUTLINE_REFINE]:
# 支持自然语言确认:"确认"、"ok"、"满意" 等关键词自动推进
refine_req = RefineRequest(session_id=request.session_id, feedback=request.content)
result = await refine_outline(refine_req)
if result.get("confirmed"):
return await generate_style(request)
return result
elif stage in [SessionStage.STYLE, SessionStage.STYLE_REFINE]:
# 同理,"生成"、"开始" 等关键词触发图片生成
...
elif stage == SessionStage.COMPLETE:
# 完成后支持 "修改第X页" 指令
if "修改第" in request.content:
match = re.search(r'修改第\s*(\d+)\s*页', request.content)
...
踩坑记录:大纲确认的阶段判断,最初是硬编码 if feedback == "确认",后来发现用户会说"ok"、"可以"、"没问题"、"通过"等各种表达。改成关键词列表匹配后体验好了很多。
难点二:Prompt Engineering —— 从"能用"到"好用"
这是整个项目花时间最多的地方。所有提示词模板集中在 modules/prompts.py 里管理。
大纲生成的 Prompt 设计
最初的 Prompt 很简单:"根据以下内容生成 PPT 大纲"。结果生成的大纲要么太泛("第一页:概述"),要么结构混乱。
最终的方案是结构化约束 + Few-shot 示例:
# modules/prompts.py
OUTLINE_PROMPT_TEMPLATE = """请根据用户输入的PPT整体的思路,梳理出每一页的核心要点。
{page_constraint}
{page_instructions}
用户的想法:
{user_input}
【示例结果格式】
第1页:核心策略总览
页面标题:2026年核心策略:防风险 + 多交易
核心要点:
关键词一:防风险
核心目标:防范尾部风险。
应对措施:系统性迭代风控系统。
关键词二:多交易
核心目标:获取绝对收益。
应对措施:开发系统化的交易信号与策略。
请按照上述格式输出每一页的大纲。同时请输出JSON格式便于程序解析:
```json
{
"pages": [{"page": 1, "theme": "...", "title": "...", "content": "..."}]
}
关键在于 Few-shot 示例用的是真实的金融场景(投资策略总览),而不是泛泛的"关于XX的介绍"。因为模型会模仿示例的深度和专业度。
图片生成的 Prompt 设计
这是最难的部分。Gemini 的图片生成能力很强,但"生成一张 PPT 页面"和"生成一张好看的图片"是完全不同的事情。
做法是先用文本模型生成每页的"设计规格说明书",再把说明书喂给图片模型:
# modules/prompts.py - 设计风格生成提示词
STYLE_GENERATION_PROMPT = """请帮我根据如下的PPT大纲,为每一页生成详细的设计方案和绘图提示词。
【配色方案规范】
{color_scheme_spec}
【字体规范】
{font_scheme_spec}
【设计原则】
{design_principles}
【PPT大纲】
{outline}
请为每一页生成:
1. 设计理念说明
2. 详细的图片生成提示词(Prompt),用于Gemini图片生成API
输出JSON格式:
{
"pages": [
{
"page": 1,
"theme": "页面主题",
"design_concept": "设计理念说明",
"prompt": "详细的图片生成提示词"
}
]
}
"""
这里还内置了具体的参考 Prompt 示例(类似 few-shot),告诉模型"好的 PPT Prompt 长什么样",包含精确到色号的配色指令和布局描述。
踩坑记录:Gemini 生成图片中的中文文字经常出错(多字、少字、乱码)。解决方案是在 Prompt 里反复强调文字内容,并要求生成 4K 分辨率 以提高文字清晰度:
# modules/gemini_api.py
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "4K"
}
}
这个问题目前没有完美解决,是 AI 图片生成的通病。
难点三:多模态输入 —— 不只是打字
这是和普通 Text-to-PPT 工具的核心区别。真实的企业场景中,用户的输入来源是多样的。
3.1 会议录音 → PPT
集成了科大讯飞非实时转写 API,支持说话人分离和金融领域优化:
# modules/asr.py
class XfyunASR:
def upload(self, num_speaker=None):
param_dict = {
'appId': self.appid,
'signa': self.signa,
'ts': self.ts,
'fileSize': file_len,
'fileName': file_name,
'duration': '200',
'roleType': 1, # 开启角色分离
'pd': 'finance', # 金融领域优化
}
if num_speaker is not None:
param_dict['roleNum'] = num_speaker # 支持用户指定说话人数
...
转写结果会区分说话人:
说话人1:我认为Q3的业绩超预期,主要受益于...
说话人2:但风险在于海外市场的不确定性...
然后在大纲生成时,录音转写内容和用户输入一起合并传入:
# server.py - 多源输入合并
combined_input = f"【用户输入的想法】\n{request.content}"
if audio_transcript:
combined_input += f"\n\n【会议录音转写内容】\n{audio_transcript}"
if support_docs_text:
combined_input += f"\n\n【支持性文档内容】\n{support_docs_text}"
3.2 支持性文档上传
支持 PDF / Word / PPT / Excel / TXT 五种格式,统一的文档抽取引擎:
# modules/doc_extract.py - 统一入口
def extract_text_from_document(file_path: str, filename: str) -> str:
ext = Path(filename).suffix.lower()
if ext == '.pdf':
return extract_text_from_pdf(file_path) # PyMuPDF
elif ext in ['.docx', '.doc']:
return extract_text_from_docx(file_path) # python-docx(含表格抽取)
elif ext in ['.pptx', '.ppt']:
return extract_text_from_pptx(file_path) # python-pptx
elif ext in ['.xlsx', '.xls']:
return extract_text_from_xlsx(file_path) # openpyxl
elif ext == '.txt':
return open(file_path, 'r', encoding='utf-8').read()
Word 文档的处理特别做了表格抽取(不只是段落文本),因为很多研报的核心数据在表格里:
def extract_text_from_docx(file_path):
doc = DocxDocument(file_path)
text_parts = []
for para in doc.paragraphs:
if para.text.strip():
text_parts.append(para.text)
# 关键:也抽取表格中的文本
for table in doc.tables:
for row in table.rows:
row_text = [cell.text.strip() for cell in row.cells if cell.text.strip()]
if row_text:
text_parts.append(" | ".join(row_text))
return "\n".join(text_parts)
3.3 逐页素材嵌入 —— 表格和图片直接上页面
这是我花了很大精力做的功能。用户可以给指定页面上传素材(图片截图、Excel 表格、甚至直接粘贴表格文本),AI 会在生成该页时直接参考这些素材。
# server.py - 页面素材上传
@app.post("/api/page-material/upload")
async def upload_page_material(
session_id: str = Form(...),
page_index: int = Form(...), # 指定第几页
file: UploadFile = File(...),
description: str = Form(default="") # 用户对素材的说明
):
# 支持图片和Excel两类
if file_ext in image_extensions:
material_type = "image"
else:
material_type = "table"
table_text = extract_table_from_file(str(material_path), file.filename) # 抽取表格内容
图片素材会作为 inline_data 直接传入 Gemini 的多模态接口,表格数据则转化为文本嵌入 Prompt。生成图片时,引擎会自动拼装:
# modules/gemini_api.py - 素材嵌入逻辑
if images_added > 0:
material_prompts.append(f"""
【用户上传的图片素材 - 最高优先级】
附件中包含用户上传的 {images_added} 个图片素材。
请务必:
1. 将这些图片素材直接复制/嵌入到生成的PPT页面中
2. 保持素材的原始内容、比例和清晰度
3. 不要对图片素材进行总结、重新绘制或简化""")
if table_texts:
material_prompts.append(f"""
【用户上传的表格数据 - 最高优先级】
请将表格数据完整呈现在PPT页面中,可以转换为美观的图表或数据可视化。
{combined_table_text}""")
效果可以看下面这张截图,表格数据被自动转成了专业的视觉呈现:
难点四:品牌风格的一致性 —— 两阶段风格迁移
企业用户最在意的是:生成的 PPT 不能一眼看出是 AI 做的,要符合公司的视觉规范。
我设计了三种模式:普通参考、母版模式、微调模式。
母版模式(重点)
用户上传公司 PPT 母版截图,后端先用 Gemini 视觉模型 分析:
# modules/gemini_api.py - 母版分析
def analyze_template_design(image_path: str) -> dict:
analysis_prompt = """你是一个专业的 PPT 设计分析师。
请分析这张PPT母版图片,返回 JSON:
{
"colors": { "background": "#...", "primary": "#...", "secondary": "#...", ... },
"fonts": { "title_style": "...", "body_style": "...", ... },
"layout": { "title_position": "...", "has_header": true, ... },
"background": { "type": "纯色/渐变/图片", "has_decorations": true, ... },
"style_summary": "整体风格总结"
}"""
# 关键:强制 JSON 输出模式
payload = {
...
"generationConfig": {
"response_mime_type": "application/json"
}
}
分析结果会以精确的色号和布局参数注入到每一页的生成 Prompt 中:
full_prompt += f"""
【母版分析结果 - 必须严格执行】
■ 配色方案(必须使用这些精确色值):
- 背景色: {colors.get('background')}
- 主色调: {colors.get('primary')}
- 辅助色: {colors.get('secondary')}
■ 布局结构:
- 标题位置: {layout.get('title_position')}
- 有顶部栏: {'是' if layout.get('has_header') else '否'}
■ 整体风格: {style_summary}
请严格按照以上规范生成,确保看起来像是同一套PPT模板的不同页面。"""
微调模式
生成完成后,点击任意一页输入修改意见(比如"把标题改成红色"),系统会把当前已生成的图片作为参考传入,只做局部修改:
# server.py - 微调并重新生成
@app.post("/api/page/refine-and-regenerate")
async def refine_page_and_regenerate(request: RefinePageRequest):
# 获取当前已生成的图片作为微调基准
current_image_path = generated_images[request.page_index]["image_path"]
# 先用文本模型修改设计方案
prompt = REFINE_PAGE_PROMPT.format(
page_num=page_num,
current_prompt=current_page.get("prompt"),
user_feedback=request.feedback
)
response_text = await generate_text(prompt)
# 再用图片模型基于当前图片微调
refine_prompt = f"""【微调模式】请基于参考图片进行微调,
用户的修改意见是:{request.feedback}
仅修改用户提到的部分,其他元素保持与参考图一致。"""
success = await generate_ppt_image(
prompt=refine_prompt,
reference_image_path=current_image_path,
reference_type="refine" # 微调模式标记
)
难点五:图片压缩和输出优化
Gemini 返回的原始图片是 PNG 格式,单张可能 5-10MB,10 页 PPT 打包下来 50MB+,在线预览也很慢。
解决方案是生成后自动压缩为 JPEG:
# modules/gemini_api.py - 图片压缩
image_bytes = base64.b64decode(image_data)
img = Image.open(io.BytesIO(image_bytes))
# RGBA → RGB(处理透明通道)
if img.mode == 'RGBA':
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
# 保存为JPEG,质量85%(约500KB-1MB/张)
output_path_jpg = str(output_path).replace('.png', '.jpg')
img.save(output_path_jpg, 'JPEG', quality=85, optimize=True)
同时支持两种下载格式:
- ZIP 打包 —— 所有页面图片
- PDF 合并 —— 用 Pillow 直接拼接图片生成 PDF,无需额外依赖
# server.py - PDF导出
@app.get("/api/download/{session_id}/pdf")
async def download_ppt_pdf(session_id: str):
first_image = valid_images[0]
first_image.save(pdf_path, "PDF", save_all=True,
append_images=valid_images[1:], resolution=150.0)
难点六:部署和超时的血泪史
核心问题:AI 图片生成很慢。单页 60-120 秒,10 页 PPT 总耗时可能超过 15 分钟。
Nginx 超时配置
默认的 60 秒 proxy_read_timeout 完全不够用:
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_connect_timeout 3600s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
# 关键:禁用缓冲,让前端能实时感知进度
proxy_buffering off;
}
踩坑记录:改了配置但不生效,排查了半天发现是 Nginx 没有正确 reload。后来发现要用 sudo nginx -T | grep proxy_read_timeout 来确认实际加载的配置,而不是看配置文件内容。
异步架构避免阻塞
FastAPI 是异步框架,但 requests.post() 是同步阻塞的。如果直接在 async def 里调用,会阻塞整个事件循环。
解决方案是用 asyncio.to_thread 把同步 HTTP 请求丢到线程池:
# modules/gemini_api.py
async def generate_text(prompt: str, thinking_level: str = "high"):
return await asyncio.to_thread(_generate_text_sync, prompt, thinking_level)
async def generate_ppt_image(prompt, output_path, ...):
return await asyncio.to_thread(_generate_ppt_image_sync, prompt, output_path, ...)
这样多个用户可以并行生成,不会互相阻塞。
重试和容错
AI API 不稳定是常态。所有外部调用都内置了重试逻辑,并且会把重试信息返回给前端:
for attempt in range(1, MAX_RETRIES + 1):
try:
response = requests.post(url, json=payload, headers=headers, timeout=180)
if response.status_code == 200:
if attempt > 1:
retry_info = f"⚠️ 接口不稳定,已在第{attempt}次重试后成功"
return True, retry_info
except requests.exceptions.Timeout:
last_error = "请求超时"
except requests.exceptions.ConnectionError:
last_error = "网络连接错误"
if attempt < MAX_RETRIES:
time.sleep(RETRY_DELAY) # 等待后重试
return False, f"❌ 失败(已重试{MAX_RETRIES}次): {last_error}"
一些细节功能
前端可编辑大纲
大纲不仅可以通过对话修改,还支持前端直接编辑后同步到后端:
@app.post("/api/outline/update")
async def update_outline(request: OutlineUpdateRequest):
session["outline_json"] = request.outline_json
# 重新生成大纲文本以保持一致
outline_text = "\n\n".join([
f"【第{i+1}页】{page.get('title')}\n{page.get('content')}"
for i, page in enumerate(request.outline_json)
])
session["outline_text"] = outline_text
自定义配色和字体方案
不止内置的"商务简约"、"酷炫技术"风格,还支持完全自定义配色和字体:
# modules/prompts.py
def build_color_scheme_spec(color_scheme: dict) -> str:
return f"""• 主色调 (Primary): {primary} —— 大标题、背景色块
• 辅助色 (Secondary): {secondary} —— 关键数据、图表高亮
• 强调色 (Accent): {accent} —— 警示、特别强调
【重要】请严格使用上述配色,不要使用其他颜色!"""
页码位置可配
page_number_position = template_settings.get("page_number_position", "bottom-center")
if page_number_position == "none":
page_number_instruction = "页面不需要显示页码。"
elif page_number_position == "bottom-left":
page_number_instruction = "左下角需要显示ppt页码。"
# ...
访问计数器 + 邀请码系统
内置了简单的访问统计和邀请码登录管理,登录记录持久化到 CSV,支持下载审计。对于想部署给团队内部用的场景很实用。
项目结构
slidebot-ai/
├── server.py # FastAPI 后端主入口 (~1200行)
├── modules/ # 模块化后端
│ ├── config.py # 配置常量(API Key、路径、默认配色)
│ ├── prompts.py # 所有提示词模板(大纲、设计、微调)
│ ├── models.py # Pydantic 数据模型
│ ├── session.py # 会话状态管理
│ ├── gemini_api.py # Gemini API 封装(文本/图片/母版分析)
│ ├── asr.py # 科大讯飞语音转写
│ ├── doc_extract.py # 文档文本抽取(PDF/Word/PPT/Excel/TXT)
│ ├── invite_codes.py # 邀请码和登录管理
│ └── visit_counter.py # 访问计数
├── frontend/
│ └── src/App.js # React 单文件前端 (~2300行)
├── .env.example # 环境变量模板
├── requirements.txt # Python 依赖
└── invite_codes.json # 邀请码配置
和初版的单文件 server.py 不同,2.0 做了模块化拆分:配置、提示词、API调用、会话管理各自独立,方便阅读和二开。
部署方式
# 1. 克隆代码
git clone https://github.com/tonyqinatcmu/SlideBot-AI.git
cd SlideBot-AI
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env,填入 GEMINI_API_KEY(必需)和讯飞 API Key(可选)
# 3. 安装后端依赖
pip install -r requirements.txt
# 4. 构建前端
cd frontend && npm install && npm run build && cd ..
# 5. 启动
python server.py
# 访问 http://localhost:8001
只有 GEMINI_API_KEY 是必填的。讯飞 API Key 不填的话,录音转写功能会自动禁用,其他功能正常使用。
效果展示
不同行业、不同风格的生成效果:
已知问题和后续计划
诚实地说,目前还有一些未解决的问题:
- 中文文字渲染:Gemini 生成图片中的中文偶尔会出错,这是 AI 图片生成模型的通病
- 生成速度:单页 60-120 秒,10 页 PPT 需要 10-20 分钟
- 并发支持:Session 存在内存中,不支持多实例部署
后续计划:
- 接入更多 AI 模型(OpenAI、Claude)
- 支持 PPTX 格式导出(目前是图片格式)
- 增加模版市场
- 支持实时协作
写在最后
这个项目最大的收获不是代码本身,而是一个认知:
AI 生成的内容永远不会一次到位,真正的价值在于让迭代成本趋近于零。
做一版大纲要 30 秒,改一版也是 30 秒。做一页 PPT 要 1 分钟,改一页也是 1 分钟。当修改的成本足够低,用户就不再纠结"第一版够不够好",而是放心大胆地说"先来一版看看"。
⭐ GitHub:github.com/tonyqinatcm…
如果觉得有帮助,欢迎 Star。有问题欢迎评论区聊,有 Bug 欢迎提 Issue。