使用Dify构建智能文档生成工作流:Word与PPT自动化指南_dify生成ppt
往期:
Docker入门
将Python Flask服务打包成Docker镜像并运行的完整指南
文章目录
- 一、工作流概述
- 二、环境准备
- 1. Dify平台准备
-
- 2. 服务部署
- 三、生成dify工作流配置
-
- 1.创建条件分支判断用户输入(word or ppt)
- 2.word分支
- 3.ppt分支
- 4输入配置
- 四、测试工作流
- 五、总结
一、工作流概述
我们的智能文档生成系统包含两个核心功能:
Word文档生成:通过API接收标题和内容,自动生成格式规范的Word文档
PPT演示文稿生成:将Markdown格式的内容自动转换为美观的PPT演示文稿
系统架构如下图所示:
[用户请求] → [Dify工作流] → [Word生成服务/PPT生成服务] → [返回下载链接]
二、环境准备
1. Dify平台准备
确保你拥有:
Dify账号(社区版或企业版)
创建工作空间的权限
API访问权限
2. 服务部署
首先需要部署两个后端服务:
Word生成服务 (wordApp.py)
from flask import Flask, request, jsonify, send_from_directoryfrom docx import Document # type: ignorefrom docx.shared import Pt # type: ignorefrom docx.enum.text import WD_ALIGN_PARAGRAPH # type: ignorefrom datetime import datetimeimport osimport jsonimport loggingapp = Flask(__name__)logging.basicConfig(level=logging.DEBUG)logger = logging.getLogger(__name__)# 修正为容器内绝对路径(对应挂载的 /app/word/temp)SAVE_DIR = \"/app/word/temp\"if not os.path.exists(SAVE_DIR): os.makedirs(SAVE_DIR, exist_ok=True) # 添加 exist_ok 处理已存在的情况@app.route(\'/gen_doc\', methods=[\'POST\'])def gen_doc(): try: data = request.get_data(as_text=True) jsObj = json.loads(data) title = jsObj.get(\'title\') content = jsObj.get(\'content\') file_name = f\"test_{datetime.now().strftime(\'%Y%m%d_%H%M%S\')}.docx\" file_path = os.path.join(SAVE_DIR, file_name) logger.debug(f\"File path: {file_path}\") doc = Document() if title: paragraph = doc.add_heading(title, level=1) paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER paragraph.style.font.name = \'FangSong\' paragraph.style.font.size = Pt(22) if content: paragraph = doc.add_paragraph(content) paragraph.style.font.name = \'FangSong\' paragraph.style.font.size = Pt(10.5) doc.save(file_path) # 返回正确的下载路径 /temp/filename return f\'Word文档已生成\\n下载链接: http://{request.host}/temp/{file_name}\' except Exception as e: return jsonify({\"error\": str(e)}), 500@app.route(\'/temp/\')def download_file(filename): # 直接使用绝对路径 SAVE_DIR return send_from_directory(SAVE_DIR, filename, as_attachment=True)if __name__ == \'__main__\': app.run(debug=True, host=\'0.0.0.0\', port=5001)
服务将运行在 http://localhost:5001
PPT生成服务 (pptAPP.py)
from flask import Flask, request, send_from_directory, send_fileimport timeimport osfrom io import BytesIOfrom pptx import Presentationfrom pptx.util import Inches, Ptfrom pptx.dml.color import RGBColorfrom pptx.enum.text import PP_ALIGNfrom pptx.enum.dml import MSO_THEME_COLORapp = Flask(__name__)# --------------------- 路径配置(与 Docker 挂载一致)---------------------# 容器内绝对路径,对应宿主机挂载的 `/app/ppt/data`SAVE_DIR = \"/app/ppt/data\"os.makedirs(SAVE_DIR, exist_ok=True)# 模板文件路径(容器内绝对路径,需提前复制到 /app/ppt/ 目录)TEMPLATE_PATH = \"/app/ppt/template.pptx\" if os.path.exists(\"/app/ppt/template.pptx\") else None# 背景图片路径(容器内绝对路径,需提前复制到 /app/ppt/ 目录)BACKGROUND_IMAGE = \"/app/ppt/background.jpg\" if os.path.exists(\"/app/ppt/background.jpg\") else None@app.route(\'/upload\', methods=[\'POST\'])def upload_markdown(): try: content = request.get_data(as_text=True) if not content: return \"No content provided\", 400 # 生成唯一文件名 file_name = f\"{int(time.time())}.md\" file_path = os.path.join(SAVE_DIR, file_name) # 使用绝对路径存储 with open(file_path, \'w\', encoding=\'utf-8\') as f: f.write(content) # 返回包含正确路径的链接(注意路由为 /data/filename) return ( f\'Markdown 文件已保存\\n\' f\'预览链接: http://{request.host}/data/{file_name}\\n\' f\'PPT下载链接: http://{request.host}/data/{file_name}?pptx\' ) except Exception as e: return f\"Error: {str(e)}\", 500@app.route(\'/data/\')def serve_file(filename): if \'pptx\' in request.args: return convert_to_pptx(filename) # 直接返回 Markdown 文件内容(如需预览) return send_from_directory(SAVE_DIR, filename, mimetype=\'text/markdown\')def convert_to_pptx(filename): file_path = os.path.join(SAVE_DIR, filename) # 使用绝对路径读取 if not os.path.exists(file_path): return \"File not found\", 404 with open(file_path, \'r\', encoding=\'utf-8\') as f: md_content = f.read() # 初始化演示文稿(使用模板或默认样式) prs = Presentation(TEMPLATE_PATH) if TEMPLATE_PATH and os.path.exists(TEMPLATE_PATH) else Presentation() if not TEMPLATE_PATH: setup_default_presentation(prs) # 解析 Markdown 内容并生成幻灯片 sections = [s for s in md_content.split(\'## \') if s.strip()] create_slides_from_sections(prs, sections) # 保存到内存缓冲区 pptx_buffer = BytesIO() prs.save(pptx_buffer) pptx_buffer.seek(0) return send_file( pptx_buffer, as_attachment=True, download_name=f\"{os.path.splitext(filename)[0]}.pptx\", mimetype=\"application/vnd.openxmlformats-officedocument.presentationml.presentation\" )def create_slides_from_sections(prs, sections): \"\"\"根据解析后的章节生成幻灯片\"\"\" if not sections: return # 处理封面幻灯片 first_section = sections[0] title, subtitle = parse_cover_section(first_section) add_cover_slide(prs, title, subtitle) # 处理内容幻灯片 for section in sections[1:]: slide_title, slide_content = parse_content_section(section) add_content_slide(prs, slide_title, slide_content)def parse_cover_section(section): \"\"\"解析封面章节(第一个标题为封面标题)\"\"\" lines = [line.strip() for line in section.split(\'\\n\') if line.strip()] title = lines[0].lstrip(\'#\').strip() if lines else \"\" subtitle = \'\\n\'.join(lines[1:]) if len(lines) > 1 else \"\" return title, subtitledef parse_content_section(section): \"\"\"解析内容章节(第一行为标题,剩余为内容)\"\"\" lines = [line.strip() for line in section.split(\'\\n\') if line.strip()] title = lines[0].lstrip(\'#\').strip() if lines else \"\" content = \'\\n\'.join(lines[1:]) if len(lines) > 1 else \"\" return title, contentdef add_cover_slide(prs, title, subtitle): \"\"\"添加封面幻灯片\"\"\" slide_layout = prs.slide_layouts[0] # 标题幻灯片布局 slide = prs.slides.add_slide(slide_layout) set_background(slide, is_cover=True) # 设置标题和副标题 if slide.shapes.title: slide.shapes.title.text = title apply_title_style(slide.shapes.title) if subtitle and len(slide.placeholders) > 1: slide.placeholders[1].text = subtitle apply_subtitle_style(slide.placeholders[1])def add_content_slide(prs, title, content): \"\"\"添加内容幻灯片(标题和内容布局)\"\"\" if not title: return slide_layout = prs.slide_layouts[1] # 标题和内容布局 slide = prs.slides.add_slide(slide_layout) set_background(slide, is_cover=False) # 设置标题和内容 if slide.shapes.title: slide.shapes.title.text = title apply_slide_title_style(slide.shapes.title) if content and len(slide.placeholders) > 1: slide.placeholders[1].text = content apply_content_style(slide.placeholders[1])def set_background(slide, is_cover=False): \"\"\"设置幻灯片背景(纯色或图片)\"\"\" background = slide.background fill = background.fill fill.solid() if is_cover: # 封面背景色(淡蓝色) fill.fore_color.rgb = RGBColor(240, 248, 255) else: # 内容页背景色(淡黄色) fill.fore_color.rgb = RGBColor(255, 253, 245)def setup_default_presentation(prs): \"\"\"设置默认演示文稿样式(16:9 比例、字体等)\"\"\" prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) # 设置母版字体样式 slide_master = prs.slide_master for layout in slide_master.slide_layouts: for shape in layout.shapes: if shape.is_placeholder: if shape.placeholder_format.type in [1, 2]: # 标题和副标题 for run in shape.text_frame.paragraphs[0].runs: run.font.name = \'微软雅黑\' run.font.color.rgb = RGBColor(50, 50, 50) elif shape.placeholder_format.type == 7: # 正文 for run in shape.text_frame.paragraphs[0].runs: run.font.name = \'微软雅黑\' run.font.size = Pt(20) run.font.color.rgb = RGBColor(80, 80, 80)def apply_title_style(shape): \"\"\"封面标题样式\"\"\" tf = shape.text_frame for paragraph in tf.paragraphs: paragraph.alignment = PP_ALIGN.CENTER for run in paragraph.runs: run.font.size = Pt(44) run.font.bold = True run.font.color.rgb = RGBColor(30, 144, 255) # 天蓝色 run.font.shadow = Truedef apply_subtitle_style(shape): \"\"\"封面副标题样式\"\"\" tf = shape.text_frame for paragraph in tf.paragraphs: paragraph.alignment = PP_ALIGN.CENTER for run in paragraph.runs: run.font.size = Pt(24) run.font.color.rgb = RGBColor(112, 128, 144) # 深灰色 run.font.italic = Truedef apply_slide_title_style(shape): \"\"\"内容页标题样式\"\"\" tf = shape.text_frame for paragraph in tf.paragraphs: paragraph.alignment = PP_ALIGN.LEFT for run in paragraph.runs: run.font.size = Pt(32) run.font.bold = True run.font.color.rgb = RGBColor(30, 144, 255) run.font.shadow = Truedef apply_content_style(shape): \"\"\"内容页正文样式\"\"\" tf = shape.text_frame for paragraph in tf.paragraphs: paragraph.alignment = PP_ALIGN.LEFT paragraph.space_before = Pt(12) paragraph.space_after = Pt(6) paragraph.line_spacing = 1.2 for run in paragraph.runs: run.font.size = Pt(20) run.font.color.rgb = RGBColor(50, 50, 50)if __name__ == \'__main__\': app.run(host=\"0.0.0.0\", port=5002, debug=True)
服务将运行在 http://localhost:5002
注意:PPT服务可以准备背景图片(background.jpg)和模板文件(template.pptx)以获得最佳效果
三、生成dify工作流配置
创建如图所示工作流dify应用:
1.创建条件分支判断用户输入(word or ppt)
2.word分支
1、LLM提示词
word:
你是一个耐心、友好、专业的智能客服助手。请根据用
用户的输入{{#1747364097761.title#}}信息和要求的字数,尽可能详细的回答用户。
2、代码执行
import redef main(query: str, answer: str) -> dict: \"\"\" 清理回答中的<think>标签和开头的空行 Args: query: 查询字符串(未使用) answer: 需要清理的回答文本 Returns: 包含清理后结果的字典 \"\"\" # 移除标签及其内容 cleaned_answer = re.sub(r\'<think[^>]*>.*?</think>\', \'\', answer, flags=re.DOTALL) # 移除开头的一个或多个换行符 final_answer = re.sub(r\'^\\n+\', \'\', cleaned_answer) return { \"result\": final_answer, }
3、HTTP请求
4、结束
3.ppt分支
1、LLM提示词
ppt:
你是一个顶尖的PPT制作专家,请根据用户的输入{{#1747364097761.title#}},打造专业且富有吸引力的PPT内容。
严格按要求生成markdown格式的ppt大纲!!
开头不用显示markdown
2、HTTP请求
3、结束
4输入配置
title: 文本标题
generate_type: 生成类型(word或者ppt)
四、测试工作流
启动两个本地服务:
python wordApp.pypython pptApp.py
word:
工作流运行结果:
ppt:
工作流运行结果:
五、总结
通过本文,你已经成功构建了一个完整的智能文档生成工作流系统。
这个系统可以:
✓ 根据模板自动生成格式规范的Word文档
✓ 将结构化内容转换为美观的PPT演示
✓ 通过Dify平台实现灵活的工作流编排
✓ 轻松集成到现有办公流程中