1. 核心实现原理

  通过Deepseek生成结构化PPT内容,再使用python-pptx库转换为实际PPT文件,主要分为两个关键步骤:

1.1 Deepseek生成PPT内容

- 设计专门的提示词模板,确保AI输出规范的JSON结构

- 控制生成内容的详细程度和格式

- 针对不同场景优化提示词

 1.2 python-pptx构建PPT

- 解析AI生成的JSON内容

- 自动创建幻灯片并填充内容

- 应用统一的样式和布局

2、核心代码实现:

2.1 前端传送ppt描述给后端

主要包括内容、题目、页数、字体、主题颜色等。例如,用户可以指定PPT的风格为专业正式风格,颜色主题为蓝色系,字体为微软雅黑等,前端将这些信息封装在一个表单对象中,然后通过HTTP POST请求发送到后端指定的接口。

  const formData = new FormData();
        formData.append('content', this.inputContent.trim());
        formData.append('title', this.pptOptions.title || '');
        formData.append('slide_count', this.pptOptions.slide_count || '15');
        formData.append('template_style', this.pptOptions.template_style || 'professional');
        formData.append('color_theme', this.pptOptions.color_theme || 'blue');
        formData.append('font_family', this.pptOptions.font_family || '微软雅黑');
        formData.append('token', this.token);
        formData.append('version', 'simple');

        const response = await fetch('http://localhost:5300/api/ppt/simple', {
          method: 'POST',
          body: formData
        });

2.2 后端处理 

2.2.1 生成JSON

构造系统提示和用户输入消息 -> 调用AI模型生成PPT内容 -> 清理和解析生成内容 -> 

验证内容结构和格式 -> 返回符合要求的PPT内容JSON

  system_prompt = f"""你是一个专业PPT内容生成助手。请生成{slide_count}页PPT内容,每页内容需要包含以下信息:

                    1. 第一页(标题页):
                    - title: 主标题
                    - subtitle: 副标题
                    - description: 简要介绍PPT的主题和目的

                    2. 第二页(目录页):
                    - title: "目录"
                    - sections: 列出所有章节标题的列表

                    3. 后续内容页:
                    每页需要包含:
                    - title: 页面标题
                    - type: 页面类型(可选值:content, two_column, image_content)
                    - points: 主要内容点列表,每个点必须是一个包含main和details的对象:
                    {{
                        "main": "主要观点",
                        "details": ["详细说明1", "详细说明2"]
                    }}
                    - notes: 演讲者注释(可选)

                    4. 最后一页(结束页):
                    - title: "总结与问答"
                    - points: 总结要点列表(使用与内容页相同的point格式)

                    请特别注意:
                    - points字段中的每个元素必须是对象,不能是纯字符串
                    - 确保所有字符串使用双引号
                    - 不要包含注释或其他非JSON内容
                    - 段落文本(40-80字)
                    - 每个页面包含3个左右段落
                    - 段落内容使用无序列表形式
                    - 每段文字后最多配一张图片
                    - 表格需要完整呈现
                    - 所有标题层级严格使用#符号标记
                    - 避免使用其他格式元素(如加粗、斜体等)

                    请以严格JSON格式返回,确保格式如下:
                    {{
                        "slides": [
                            {{
                                "title": "主标题",
                                "subtitle": "副标题",
                                "description": "简介"
                            }},
                            {{
                                "title": "目录",
                                "sections": ["章节1", "章节2"]
                            }},
                            {{
                                "title": "章节标题",
                                "type": "content",
                                "points": [
                                    {{
                                        "main": "主要观点",
                                        "details": ["详细说明1", "详细说明2"]
                                    }}
                                ],
                                "notes": "演讲者注释"
                            }}
                        ]
                    }}
                    """
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"标题:{title}\n内容:{content}"}
        ]

        try:
            response = self.client.chat.completions.create(
                model="deepseek-reasoner",
                messages=messages,
                temperature=0.1,  # 降低温度,使生成更加确定性和详细
                max_tokens=8192,  # 增加最大token数,允许生成更多内容
                top_p=0.95        # 调整top_p参数,保持一定的创造性同时确保质量
            )

            if not response.choices or len(response.choices) == 0:
                raise ValueError("API响应中没有生成内容")

            generated_content = response.choices[0].message.content
            if not generated_content:
                raise ValueError("API生成的内容为空")

            # 清理和解析JSON内容
            try:
                content = generated_content.strip()
                if content.startswith("```json"):
                    content = content[7:]
                elif content.startswith("```"):
                    content = content[3:]
                if content.endswith("```"):
                    content = content[:-3]

                content = content.strip()
                logger.info(f"清理后的内容: {content}")

                ppt_content = json.loads(content)

                # 验证内容结构
                if not isinstance(ppt_content, dict) or 'slides' not in ppt_content:
                    raise ValueError("生成的内容缺少必要的'slides'字段")

                if not isinstance(ppt_content['slides'], list):
                    raise ValueError("'slides'字段必须是列表类型")

                # 验证points结构
                for slide in ppt_content['slides']:
                    if 'points' in slide:
                        for point in slide['points']:
                            if not isinstance(point, dict) or 'main' not in point:
                                raise ValueError("points中的元素必须包含'main'字段的字典")

                return ppt_content

            except json.JSONDecodeError as e:
                logger.error(f"JSON解析失败: {str(e)}")
                logger.error(f"原始内容: {generated_content}")
                raise ValueError(f"生成的内容不是有效的JSON格式: {str(e)}")

        except Exception as e:
            logger.error(f"生成PPT内容失败: {str(e)}", exc_info=True)
            raise
2.2.2 生成ppt

  利用python-pptx库生成实际的PPT文件,过程包括创建一个新的演示文稿对象,设置幻灯片的尺寸为适合现代屏幕的16:9宽高比,然后根据JSON内容中的每页幻灯片数据,逐个创建幻灯片并填充内容。对于不同类型的幻灯片(如两栏布局),会应用相应的布局和样式。 

 file_id = uuid.uuid4().hex
        base_name = f"{title}_{file_id}"
        pptx_path = os.path.join(self.upload_folder, f"{base_name}.pptx")

        try:
            prs = Presentation()
            prs.slide_width = Inches(16)
            prs.slide_height = Inches(9)

            for slide_data in ppt_content.get('slides', []):
                if slide_data.get('type') == 'two_column':
                    slide = prs.slides.add_slide(prs.slide_layouts[6])
                    title_shape = slide.shapes.add_textbox(
                        Inches(1), Inches(0.5), Inches(14), Inches(1)
                    )
                else:
                    slide = prs.slides.add_slide(prs.slide_layouts[1])
                    title_shape = slide.shapes.title

                if title_shape:
                    title_shape.text = slide_data.get('title', '')
                    title_frame = title_shape.text_frame
                    title_paragraph = title_frame.paragraphs[0]
                    title_paragraph.alignment = PP_ALIGN.CENTER
                    title_run = title_paragraph.runs[0]
                    title_run.font.size = Pt(44)
                    title_run.font.bold = True
                    title_run.font.color.rgb = self.COLORS['primary']

                if 'subtitle' in slide_data:
                    if len(slide.placeholders) > 1:
                        subtitle = slide.placeholders[1]
                        subtitle.text = slide_data['subtitle']
                        subtitle_frame = subtitle.text_frame
                        subtitle_paragraph = subtitle_frame.paragraphs[0]
                        subtitle_paragraph.alignment = PP_ALIGN.CENTER
                        subtitle_run = subtitle_paragraph.runs[0]
                        subtitle_run.font.size = Pt(24)
                        subtitle_run.font.italic = True
                        subtitle_run.font.color.rgb = self.COLORS['secondary']

                elif 'sections' in slide_data:
                    if len(slide.placeholders) > 1:
                        content = slide.placeholders[1]
                        tf = content.text_frame
                        for section in slide_data['sections']:
                            p = tf.add_paragraph()
                            p.text = f"• {section}"
                            p.font.size = Pt(24)
                            p.font.color.rgb = self.COLORS['secondary']
                            p.space_after = Pt(12)

                elif 'points' in slide_data:
                    if slide_data.get('type') == 'two_column':
                        left_content = slide.shapes.add_textbox(
                            Inches(1), Inches(2), Inches(6.5), Inches(5)
                        )
                        right_content = slide.shapes.add_textbox(
                            Inches(8.5), Inches(2), Inches(6.5), Inches(5)
                        )
                        mid_point = len(slide_data['points']) // 2
                        left_points = slide_data['points'][:mid_point]
                        right_points = slide_data['points'][mid_point:]
                        self._add_points_to_textbox(left_content.text_frame, left_points)
                        self._add_points_to_textbox(right_content.text_frame, right_points)
                    else:
                        if len(slide.placeholders) > 1:
                            content = slide.placeholders[1]
                            self._add_points_to_textbox(content.text_frame, slide_data['points'])

                if 'notes' in slide_data:
                    notes_slide = slide.notes_slide
                    notes_slide.notes_text_frame.text = slide_data['notes']

            prs.save(pptx_path)
            logger.info(f"PPT文件已保存到:{pptx_path}")
            return pptx_path

        except Exception as e:
            logger.error(f"生成PPT文件失败:{str(e)}", exc_info=True)
            raise

    def _add_points_to_textbox(self, text_frame, points: List[Dict[str, Any]]) -> None:
        """向文本框添加要点内容(增强容错处理)"""
        for raw_point in points:
            # 容错处理:如果point是字符串,转换为字典
            if isinstance(raw_point, str):
                point = {'main': raw_point, 'details': []}
            else:
                point = raw_point

            # 添加主要观点
            p = text_frame.add_paragraph()
            p.text = f"• {point.get('main', '')}"
            p.font.size = Pt(28)
            p.font.bold = True
            p.font.color.rgb = self.COLORS['primary']
            p.space_after = Pt(12)

            # 添加详细说明
            for detail in point.get('details', []):
                p = text_frame.add_paragraph()
                p.text = f"  - {detail}"
                p.font.size = Pt(20)
                p.font.color.rgb = self.COLORS['secondary']
                p.space_after = Pt(8)
2.2.3 传送

  返回包含文件路径和文件名的字典,之后便于前端客户端完成下载。


            filename = os.path.basename(pptx_path)
            return {
                "file_path": pptx_path,
                "filename": filename,
                "status": "success"
            }

2.3 接收

前端在接收到后端返回的响应后,会以blob格式接收存储文件,并触发下载操作。为了确保资源的合理使用,下载完成后需要注意释放Blob URL。

  if (response.ok) {
          // 直接处理文件响应
          const blob = await response.blob();
          // 从响应头获取文件名,或使用默认文件名
          const filename = this.getFilenameFromResponse(response) || `${this.pptOptions.title || 'presentation'}.pptx`;
          // 创建一个临时URL
          const url = window.URL.createObjectURL(blob);
          this.downloadUrl = url;
          // 触发下载
          this.downloadPPT(url, filename);
          // 显示成功消息
          this.$nextTick(() => {
            this.errorMessage = '';
          });
 downloadPPT(url, filename) {
      // 创建一个隐藏的a标签来触发下载
      const a = document.createElement('a');
      a.style.display = 'none';
      a.href = url;
      a.download = filename || `${this.pptOptions.title || 'presentation'}.pptx`;
      document.body.appendChild(a);
      a.click();

      // 下载开始后移除元素
      setTimeout(() => {
        document.body.removeChild(a);

        // 如果是Blob URL,需要释放
        if (url.startsWith('blob:')) {
          window.URL.revokeObjectURL(url);
        }
      }, 100);

      // 显示成功消息
      this.$nextTick(() => {
        this.errorMessage = '';
        // 设置下载URL以显示成功消息
        if (!this.downloadUrl) {
          this.downloadUrl = url;
        }
      });
    }

3、优化方向

3.1 提示词优化

  为了进一步提高生成内容的质量,计划增加few-shot prompt示例,这种示例提示词能够为模型提供更明确的输出方向和格式要求,从而生成更符合预期的内容。同时,优化内容长度控制参数,使生成的幻灯片内容既全面又不至于过于冗长。

3.2 python-pptx生成过程优化

在PPT生成过程中,计划预定义模板,用户可以根据自己的喜好和需求选择不同的模板,这将大大提高PPT的视觉吸引力和专业度;计划实现智能布局调整功能,根据内容的多少和类型自动优化幻灯片的布局,以确保内容的合理分布和最佳展示效果。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐