PPT与播客智能生成系统设计与实现

PPT与播客智能生成系统设计与实现

本文详细介绍了创意工坊(Creative Workshop)模块中PPT演示文稿和播客音频的智能生成系统,从需求分析、架构设计到核心实现,全面记录了整个开发过程。

目录


一、项目背景与需求分析

1.1 项目背景

在信息爆炸的时代,用户面临着大量的学习资料、会议记录、文档等内容需要消化和整理。传统的内容消费方式效率低下,用户希望能够:

  1. 快速理解内容:通过AI生成的概览快速把握核心要点
  2. 多形式输出:将同一份内容转换为不同的媒体形式(PPT、播客、视频等)
  3. 个性化定制:根据不同场景选择不同的风格和形式

基于以上需求,我们设计并实现了**创意工坊(Creative Workshop)**模块,这是一个多模态内容生成平台,支持将用户上传的文档、笔记等内容智能转换为多种媒体形式。

1.2 功能需求

创意工坊支持以下媒体类型的生成:

媒体类型 代码 图标 文件格式 描述
播客音频 audio 🎧 mp3/wav 生成对话式播客音频
视频 video 🎬 mp4 生成讲解视频
PPT演示 ppt 📊 pptx 生成演示文稿
测验题目 quiz 📝 json 生成测验题目
思维导图 mindmap 🧠 md 生成思维导图

1.3 核心用户流程

复制代码
创建工坊 → 添加来源 → 生成概览 → 选择媒体类型 → 配置参数 → 生成脚本(SSE) → 预览 → 创建任务(SSE) → 完成

详细流程说明:

  1. 创建工坊:用户创建一个新的创意工坊,作为内容创作的容器
  2. 添加来源:支持上传文件(PDF、Word、Markdown等)或关联已有笔记
  3. 生成概览:AI分析所有来源内容,生成工坊概览和推荐问题
  4. 选择媒体类型:用户选择要生成的媒体类型(PPT、播客等)
  5. 配置参数:根据媒体类型配置相应参数(如播客风格、PPT详细度等)
  6. 生成脚本:AI生成大纲和脚本,通过SSE流式返回
  7. 预览确认:用户预览生成的大纲和脚本
  8. 创建任务:确认后创建生成任务,通过SSE实时推送进度
  9. 完成下载:任务完成后可下载或在线预览生成的媒体文件

二、系统架构设计

2.1 整体架构

创意工坊采用分层架构设计,主要包含以下层次:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      前端应用层                              │
│                   (Flutter/Web Client)                       │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      API网关层                               │
│                  (Spring Boot Controller)                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              CreativeWorkshopController              │   │
│  │  - 工坊管理 API                                      │   │
│  │  - 来源管理 API                                      │   │
│  │  - 脚本生成 API (SSE)                               │   │
│  │  - 任务管理 API (SSE)                               │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      业务服务层                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │           CreativeWorkshopServiceImpl               │   │
│  │  - 工坊/来源/任务管理                               │   │
│  │  - AI大纲/脚本生成                                  │   │
│  │  - 媒体生成调度                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│  ┌──────────────────┐  ┌──────────────────┐               │
│  │ PptEnhanced      │  │ PodcastService   │               │
│  │ SlideStructure   │  │ Impl             │               │
│  │ Generator        │  │                  │               │
│  └──────────────────┘  └──────────────────┘               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      外部服务层                              │
│  ┌──────────────────┐  ┌──────────────────┐               │
│  │   AI Model API   │  │  SoulX-Podcast   │               │
│  │  (yantronic-m1)  │  │      API         │               │
│  └──────────────────┘  └──────────────────┘               │
│  ┌──────────────────┐  ┌──────────────────┐               │
│  │    S3 Storage    │  │   FeiShu Alert   │               │
│  │   (EBCloud S3)   │  │     Service      │               │
│  └──────────────────┘  └──────────────────┘               │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      数据持久层                              │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    MySQL Database                     │  │
│  │  - creative_workshop (工坊表)                        │  │
│  │  - creative_source (来源表)                          │  │
│  │  - creative_task (任务表)                            │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2.2 核心组件说明

2.2.1 CreativeWorkshopServiceImpl

这是创意工坊的核心服务实现类,负责:

  • 工坊管理:创建、查询、删除工坊
  • 来源管理:添加、删除、排序来源文件
  • 概览生成:调用AI生成工坊概览
  • 脚本生成:生成大纲和脚本
  • 任务调度:根据媒体类型调度不同的生成服务
java 复制代码
// 核心配置
private static final String AI_MODEL = "";  // AI模型(128k上下文)
private static final int MAX_RETRY_COUNT = 3;                 // 最大重试次数
private static final long RETRY_DELAY_MS = 2000;              // 重试延迟
2.2.2 PptEnhancedSlideStructureGenerator

PPT页面结构生成器,负责:

  • AI布局选择:使用AI智能选择每页的布局类型
  • 规则回退:AI失败时使用规则模式生成
  • 配图提示词:为需要配图的页面生成英文提示词
2.2.3 PodcastServiceImpl

播客服务实现,负责:

  • 脚本转换:将脚本转换为对话格式
  • 异步任务提交:调用SoulX-Podcast API
  • 状态轮询:轮询任务状态直到完成
  • 音频上传:将生成的音频上传到S3

2.3 技术栈

层次 技术选型
后端框架 Spring Boot 3.x
ORM框架 MyBatis-Plus
数据库 MySQL 8.0
对象存储 EBCloud S3
AI模型 deepseek (128k context)
播客服务 SoulX-Podcast (Python)
实时通信 Server-Sent Events (SSE)
HTTP客户端 WebClient (Reactive)

三、数据库设计

3.1 ER图

复制代码
┌─────────────────────┐       ┌─────────────────────┐
│  creative_workshop  │       │   creative_source   │
├─────────────────────┤       ├─────────────────────┤
│ id (PK)             │───┐   │ id (PK)             │
│ uid                 │   │   │ source_id (UK)      │
│ title               │   │   │ workshop_id (FK)────┼───┐
│ icon                │   │   │ uid                 │   │
│ summary             │   │   │ source_type         │   │
│ suggested_questions │   │   │ file_key            │   │
│ cover_image         │   │   │ note_id             │   │
│ source_count        │   │   │ content             │   │
│ total_word_count    │   │   │ selected            │   │
│ summary_generate_   │   │   │ sort_order          │   │
│   time              │   │   │ status              │   │
│ status              │   │   │ create_time         │   │
│ create_time         │   │   │ update_time         │   │
│ update_time         │   │   └─────────────────────┘   │
└─────────────────────┘   │                             │
                          │   ┌─────────────────────┐   │
                          │   │   creative_task     │   │
                          │   ├─────────────────────┤   │
                          │   │ id (PK)             │   │
                          │   │ task_id (UK)        │   │
                          └───┼─workshop_id (FK)    │   │
                              │ uid                 │   │
                              │ title               │   │
                              │ media_type          │   │
                              │ audio_style         │   │
                              │ voice_preference    │   │
                              │ video_style         │   │
                              │ ppt_style           │   │
                              │ outline_json        │   │
                              │ script_json         │   │
                              │ status              │   │
                              │ progress            │   │
                              │ file_key            │   │
                              │ duration            │   │
                              │ slide_count         │   │
                              │ error_message       │   │
                              │ create_time         │   │
                              │ update_time         │   │
                              │ complete_time       │   │
                              └─────────────────────┘

3.2 表结构详解

3.2.1 creative_workshop(工坊表)
sql 复制代码
CREATE TABLE `creative_workshop` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID(同时作为工坊ID)',
  `uid` varchar(64) NOT NULL COMMENT '用户ID',
  `title` varchar(255) DEFAULT '未命名工坊' COMMENT '工坊标题',
  `icon` varchar(32) DEFAULT NULL COMMENT '工坊图标(emoji)',
  `summary` text COMMENT '工坊概览/摘要',
  `suggested_questions` text COMMENT 'AI生成的推荐问题(JSON数组)',
  `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图片URL',
  `source_count` int DEFAULT NULL COMMENT '生成概览时的来源文件数量',
  `total_word_count` int DEFAULT NULL COMMENT '生成概览时的总字数',
  `summary_generate_time` datetime DEFAULT NULL COMMENT '概览生成时间',
  `summary_like_status` int DEFAULT 0 COMMENT '概览点赞状态:0-未操作,1-点赞,-1-踩',
  `summary_feedback` text COMMENT '概览反馈内容',
  `status` int DEFAULT 1 COMMENT '状态:0-已删除,1-正常',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='创意工坊表';
3.2.2 creative_source(来源表)
sql 复制代码
CREATE TABLE `creative_source` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `source_id` varchar(64) NOT NULL COMMENT '来源ID(业务主键)',
  `workshop_id` bigint NOT NULL COMMENT '工坊ID',
  `uid` varchar(64) NOT NULL COMMENT '用户ID',
  `source_type` varchar(32) NOT NULL COMMENT '来源类型:file/note/text',
  `source_name` varchar(255) DEFAULT NULL COMMENT '来源名称',
  `file_key` varchar(255) DEFAULT NULL COMMENT '文件Key(S3存储路径)',
  `note_id` int DEFAULT NULL COMMENT '笔记ID(关联note表)',
  `content` text COMMENT '文本内容(text类型使用)',
  `selected` int DEFAULT 1 COMMENT '是否选中:0-未选中,1-选中',
  `sort_order` int DEFAULT 0 COMMENT '排序顺序',
  `status` int DEFAULT 1 COMMENT '状态:0-已删除,1-正常',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_source_id` (`source_id`),
  KEY `idx_workshop_id` (`workshop_id`),
  KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='创意工坊来源表';
3.2.3 creative_task(任务表)
sql 复制代码
CREATE TABLE `creative_task` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `task_id` varchar(64) NOT NULL COMMENT '任务ID(业务主键)',
  `workshop_id` bigint NOT NULL COMMENT '工坊ID',
  `uid` varchar(64) NOT NULL COMMENT '用户ID',
  `title` varchar(255) DEFAULT NULL COMMENT '作品标题',
  `media_type` varchar(32) NOT NULL COMMENT '媒体类型:audio/video/ppt/quiz/mindmap',
  `audio_style` varchar(32) DEFAULT NULL COMMENT '音频风格:qa/debate/overview/review',
  `voice_preference` varchar(32) DEFAULT NULL COMMENT '声音偏好',
  `video_style` varchar(32) DEFAULT NULL COMMENT '视频风格:detailed/simple',
  `ppt_style` varchar(32) DEFAULT NULL COMMENT 'PPT风格:detailed/simple',
  `mindmap_style` varchar(32) DEFAULT NULL COMMENT '思维导图风格',
  `quiz_count` int DEFAULT NULL COMMENT '测验题目数量',
  `quiz_difficulty` varchar(32) DEFAULT NULL COMMENT '测验难度',
  `ai_instructions` text COMMENT 'AI指令/自定义要求',
  `outline_json` text COMMENT '大纲JSON',
  `outline_markdown` text COMMENT '大纲Markdown',
  `script_json` text COMMENT '脚本JSON',
  `source_count` int DEFAULT NULL COMMENT '使用的来源数量',
  `status` int DEFAULT 0 COMMENT '任务状态:0-待处理,1-处理中,2-已完成,3-失败',
  `progress` int DEFAULT 0 COMMENT '处理进度(0-100)',
  `file_key` varchar(255) DEFAULT NULL COMMENT '结果文件Key',
  `duration` int DEFAULT NULL COMMENT '时长(秒)',
  `slide_count` int DEFAULT NULL COMMENT '幻灯片数量',
  `error_message` text COMMENT '错误信息',
  `deleted` int DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `complete_time` datetime DEFAULT NULL COMMENT '完成时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_task_id` (`task_id`),
  KEY `idx_workshop_id` (`workshop_id`),
  KEY `idx_uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='创意工坊任务表';

3.3 状态枚举定义

任务状态(WorkshopTaskStatusEnum)
状态码 名称 说明
0 PENDING 待处理(脚本已生成,等待用户确认)
1 PROCESSING 处理中(正在生成媒体文件)
2 COMPLETED 已完成
3 FAILED 失败
媒体类型(WorkshopMediaTypeEnum)
代码 名称 图标 文件扩展名
audio 播客音频 🎧 mp3/wav
video 视频 🎬 mp4
ppt PPT演示 📊 pptx
quiz 测验题目 📝 json
mindmap 思维导图 🧠 md

四、API接口设计

4.1 接口概览

创意工坊API采用RESTful风格设计,所有接口以 /workshop 为前缀。

复制代码
/workshop
├── /create                          POST    创建工坊
├── /list                            GET     获取工坊列表
├── /{workshopId}                    GET     获取工坊详情
├── /{workshopId}                    DELETE  删除工坊
├── /{workshopId}/rename             PUT     重命名工坊
├── /{workshopId}/source             POST    添加来源
├── /{workshopId}/source/list        GET     获取来源列表
├── /{workshopId}/source/select      PUT     更新来源选中状态
├── /{workshopId}/source/{sourceId}  DELETE  删除来源
├── /{workshopId}/summary/generate   POST    生成概览 (SSE)
├── /{workshopId}/summary            GET     获取概览
├── /{workshopId}/script/generate    POST    生成脚本 (SSE)
├── /{workshopId}/script/{scriptId}  GET     获取脚本详情
├── /{workshopId}/task/create        POST    创建任务 (SSE)
├── /{workshopId}/task/list          GET     获取任务列表
├── /{workshopId}/task/{taskId}      GET     获取任务详情
├── /{workshopId}/task/{taskId}/status GET   获取任务状态
└── /media/options                   GET     获取媒体类型选项

4.2 核心接口详解

4.2.1 生成脚本接口(SSE)

请求

http 复制代码
POST /workshop/{workshopId}/script/generate
Content-Type: application/json
Accept: text/event-stream

{
  "mediaType": "audio",
  "audioStyle": "qa",
  "voicePreference": "professional",
  "scriptId": null  // 重新生成时传入已有scriptId
}

SSE事件流

复制代码
event: status
data: {"id":123,"type":"status","phase":"outline","message":"正在分析来源内容...","progress":5}

event: status
data: {"id":123,"type":"status","phase":"outline","message":"正在生成大纲...","progress":15}

event: content
data: {"id":123,"type":"content","text":"# 大纲标题\n\n## 第一章节\n..."}

event: status
data: {"id":123,"type":"status","phase":"script","message":"正在生成脚本...","progress":60}

event: complete
data: {"id":123,"type":"complete","scriptId":"script_xxx","segments":[...]}

data: [DONE]
4.2.2 创建任务接口(SSE)

请求

http 复制代码
POST /workshop/{workshopId}/task/create
Content-Type: application/json
Accept: text/event-stream

{
  "scriptId": "script_xxx"
}

SSE事件流

复制代码
event: progress
data: {"taskId":"script_xxx","type":"progress","message":"正在准备生成...","progress":5}

event: progress
data: {"taskId":"script_xxx","type":"progress","message":"正在生成PPT...","progress":20}

event: progress
data: {"taskId":"script_xxx","type":"progress","message":"正在生成第3页...","progress":45}

event: complete
data: {"taskId":"script_xxx","type":"complete","progress":100,"message":"生成完成","resultFileKey":"xxx","slideCount":15}

data: [DONE]

4.3 音频风格参数说明

风格代码 名称 说明 说话人数
qa 问答访谈 主持人提问,嘉宾回答 2人
debate 辩论讨论 双方观点交锋 2人
overview 概述讲解 单人清晰概括内容 1人
review 评价分析 单人深度分析评价 1人

4.4 PPT风格参数说明

风格代码 名称 说明
detailed 详细版 内容丰富,每页4-6个要点,适合深度学习
simple 简洁版 精简核心,每页2-3个要点,适合快速浏览

五、核心功能实现:PPT生成

5.1 PPT生成流程

复制代码
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  生成大纲   │───▶│  生成页面   │───▶│  渲染PPT    │───▶│  上传S3     │
│  (AI)       │    │  结构(AI)   │    │  (Apache    │    │             │
│             │    │             │    │   POI)      │    │             │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘

详细流程说明:

  1. 生成大纲(AI):根据来源内容,使用AI生成结构化的PPT大纲,包含章节、要点和详细说明
  2. 生成页面结构(AI):使用AI智能选择每页的布局类型,生成完整的页面结构JSON
  3. 渲染PPT(Apache POI):根据页面结构,使用Apache POI库渲染实际的PPTX文件
  4. 上传S3:将生成的PPT文件上传到对象存储

5.2 大纲数据结构

json 复制代码
{
  "title": "PPT标题",
  "subtitle": "副标题",
  "slideCount": 15,
  "nodes": [
    {
      "id": "1",
      "title": "章节标题",
      "keyPoints": [
        "要点1标题",
        "要点2标题"
      ],
      "details": [
        ["要点1的详细说明1", "要点1的详细说明2"],
        ["要点2的详细说明1", "要点2的详细说明2"]
      ]
    }
  ]
}

5.3 AI驱动的页面结构生成

PptEnhancedSlideStructureGenerator 是PPT生成的核心组件,采用AI优先、规则兜底的策略,负责将大纲转换为具体的页面结构。

5.3.1 生成策略
java 复制代码
@Value("${ppt.generation.enable-ai-layout:true}")
private boolean enableAILayout;

public List<PptSlideStructure> generateSlideStructures(String title, String subtitle,
                                                        List<Map<String, Object>> nodes,
                                                        boolean enableImages,
                                                        ProgressCallback progressCallback) {
    if (enableAILayout) {
        try {
            // 使用AI生成完整的PPT结构
            callback.onProgress("正在使用AI生成页面结构...", 18);
            return generateSlideStructuresWithAI(title, subtitle, nodes, enableImages, callback);
        } catch (Exception e) {
            log.error("AI生成PPT结构失败,回退到规则模式", e);
            callback.onProgress("AI生成失败,使用规则模式...", 20);
            return generateSlideStructuresWithRules(title, subtitle, nodes, enableImages);
        }
    } else {
        // 使用规则模式
        return generateSlideStructuresWithRules(title, subtitle, nodes, enableImages);
    }
}

策略说明:

  • AI模式(默认):调用大模型智能选择每页的布局类型,生成更专业的PPT结构
  • 规则模式(兜底):当AI调用失败时,使用预定义的规则生成布局
  • 可配置 :通过配置项 ppt.generation.enable-ai-layout 控制是否启用AI模式
5.3.2 支持的布局类型

封面布局

  • cover_diagonal - 斜切设计
  • cover_centered - 居中设计
  • cover_bottom_bar - 底部色条
  • cover_gradient_side - 渐变侧边
  • cover_minimal - 极简设计
  • cover_circles - 圆形装饰
  • cover_geometric - 几何图形

目录布局

  • toc_numbered - 编号列表
  • toc_left_block - 左侧色块
  • toc_grid_cards - 网格卡片
  • toc_timeline - 时间线
  • toc_icons - 图标列表

内容布局

  • text_only - 纯文本
  • numbered_list - 编号列表
  • icon_list - 图标列表
  • three_cards - 三卡片
  • four_cards_grid - 四卡片网格
  • comparison_cards - 对比卡片
  • timeline_horizontal - 水平时间线
  • flow_chart - 流程图
  • pyramid - 金字塔
  • left_image_right_text - 左图右文
  • tech_side_bar - 科技侧边栏
  • rich_content_split - 富内容分割

章节分隔布局

  • section_numbered - 编号章节
  • section_centered - 居中章节
  • section_gradient - 渐变章节
  • section_left_block - 左侧色块

结尾布局

  • ending_centered - 居中结尾
  • ending_contact - 联系方式
  • ending_summary - 总结回顾
5.3.3 AI布局生成系统提示词

AI布局生成使用专门设计的系统提示词,指导大模型生成符合要求的页面结构:

复制代码
你是一个专业的PPT页面结构设计专家。你的任务是根据给定的PPT大纲,为每一页选择最合适的布局类型,并生成完整的页面结构JSON。

## 你的能力
1. 根据内容特点选择最佳布局(图表、卡片、时间线、对比等)
2. 确保布局多样性,避免连续使用相同布局
3. 为需要配图的页面生成英文配图提示词
4. 保证内容完整性,不遗漏任何大纲内容

## 可用的布局类型

### 封面布局
- cover_diagonal: 斜切设计,现代感强
- cover_centered: 居中设计,简洁大方
- cover_bottom_bar: 底部色条,商务风格
- cover_gradient_side: 渐变侧边,创意风格
- cover_minimal: 极简设计,专业感
- cover_circles: 圆形装饰,活泼风格
- cover_geometric: 几何图形,科技感

### 目录布局
- toc_numbered: 编号列表,清晰有序
- toc_left_block: 左侧色块,层次分明
- toc_grid_cards: 网格卡片,适合3-4项
- toc_timeline: 时间线,适合流程
- toc_icons: 图标列表,视觉丰富

### 内容布局
- text_only: 纯文本,适合大段文字
- numbered_list: 编号列表,适合步骤
- icon_list: 图标列表,视觉引导
- three_cards: 三卡片,适合3个要点
- four_cards_grid: 四卡片网格,适合4个要点
- comparison_cards: 对比卡片,适合优劣对比
- timeline_horizontal: 水平时间线,适合发展历程
- flow_chart: 流程图,适合步骤流程
- pyramid: 金字塔,适合层级关系
- left_image_right_text: 左图右文,图文结合
- tech_side_bar: 科技侧边栏,适合技术内容
- rich_content_split: 富内容分割,适合深度分析

## 输出格式
返回JSON对象,包含cover、toc、sections、ending四个部分...
5.3.4 用户提示词构建
java 复制代码
private String buildUserPromptForAI(String title, String subtitle,
                                     List<Map<String, Object>> nodes, boolean enableImages) {
    StringBuilder sb = new StringBuilder();
    sb.append("请为以下PPT生成完整的页面结构:\n\n");
    sb.append("## PPT信息\n");
    sb.append("- 标题:").append(title).append("\n");
    sb.append("- 是否启用配图:").append(enableImages ? "是" : "否").append("\n\n");
    
    sb.append("## 大纲内容\n\n");
    // 输出每个章节的keyPoints和details
    for (int i = 0; i < nodes.size(); i++) {
        Map<String, Object> node = nodes.get(i);
        String sectionTitle = (String) node.getOrDefault("title", "章节 " + (i + 1));
        sb.append("### 章节 ").append(i + 1).append(": ").append(sectionTitle).append("\n");
        
        List<String> keyPoints = (List<String>) node.get("keyPoints");
        List<List<String>> details = (List<List<String>>) node.get("details");
        
        if (keyPoints != null) {
            for (int j = 0; j < keyPoints.size(); j++) {
                sb.append("- **").append(keyPoints.get(j)).append("**\n");
                if (details != null && j < details.size()) {
                    for (String detail : details.get(j)) {
                        sb.append("  - ").append(detail).append("\n");
                    }
                }
            }
        }
    }
    
    sb.append("\n## 内容完整性要求(最高优先级!)\n");
    sb.append("1. 大纲中的每个keyPoint都必须出现在PPT中\n");
    sb.append("2. 每个keyPoint对应的所有details都必须完整保留\n");
    sb.append("3. 禁止简化、合并或省略任何内容\n");
    sb.append("4. 如果一个章节内容太多,必须拆分为多个内容页\n");
    
    return sb.toString();
}
5.3.5 AI响应解析

AI返回的JSON结构示例:

json 复制代码
{
  "cover": {
    "title": "人工智能发展趋势",
    "subtitle": "2024年度技术报告",
    "layout": "cover_geometric",
    "image_needed": false
  },
  "toc": {
    "layout": "toc_grid_cards",
    "items": ["技术背景", "核心突破", "应用场景", "未来展望"]
  },
  "sections": [
    {
      "section_page": {
        "title": "技术背景",
        "layout": "section_numbered"
      },
      "content_pages": [
        {
          "title": "大语言模型的演进",
          "layout": "timeline_horizontal",
          "points": [
            {
              "point": "GPT系列的发展",
              "details": ["从GPT-1到GPT-4的技术演进", "参数规模从1.17亿到1.76万亿"]
            },
            {
              "point": "开源模型的崛起",
              "details": ["LLaMA、Mistral等开源模型", "推动了AI民主化进程"]
            }
          ],
          "image_needed": false
        }
      ]
    }
  ],
  "ending": {
    "title": "感谢观看",
    "subtitle": "由y笔记智能生成",
    "layout": "ending_centered"
  }
}
5.3.6 智能布局选择算法(规则模式)
java 复制代码
private String selectSmartLayout(List<String> points, int index, boolean enableImages,
                                  String[] chartLayouts, String[] infoLayouts,
                                  String[] cardLayouts, String[] textLayouts,
                                  String[] imageLayouts) {
    int pointCount = points.size();
    
    // 检查内容特点
    boolean hasNumbers = points.stream().anyMatch(p -> p.matches(".*\\d+.*"));
    boolean hasPercentage = points.stream().anyMatch(p -> p.contains("%"));
    boolean hasSteps = points.stream().anyMatch(p -> 
            p.contains("步骤") || p.contains("流程"));
    boolean hasComparison = points.stream().anyMatch(p -> 
            p.contains("对比") || p.contains("优势") || p.contains("劣势"));
    boolean hasTimeline = points.stream().anyMatch(p -> 
            p.contains("年") || p.contains("阶段"));
    boolean isTechnical = points.stream().anyMatch(p -> 
            p.contains("技术") || p.contains("架构") || p.contains("API"));
    
    // 根据特点选择布局
    if (hasPercentage && pointCount <= 5) {
        return "progress_bars";  // 有百分比,使用进度条
    } else if (hasTimeline) {
        return "timeline_horizontal";  // 有时间线特征
    } else if (hasSteps) {
        return "flow_chart";  // 有步骤,使用流程图
    } else if (hasComparison) {
        return "comparison_cards";  // 有对比,使用对比卡片
    } else if (isTechnical && pointCount >= 3) {
        return "tech_side_bar";  // 技术内容,使用科技侧边栏
    } else if (pointCount == 3) {
        return "three_cards";  // 3个要点,使用三卡片
    } else if (pointCount == 4) {
        return "four_cards_grid";  // 4个要点,使用四卡片
    } else {
        return "numbered_list";  // 默认使用编号列表
    }
}
5.3.7 章节布局多样化

为确保PPT视觉效果的多样性,系统会强制对章节分隔页使用不同的布局:

java 复制代码
/**
 * 强制章节布局多样化
 * 根据章节编号轮换使用不同的布局
 */
private String enforceSectionLayoutDiversity(String originalLayout, int sectionNumber) {
    String[] sectionLayouts = {"section_numbered", "section_centered",
                               "section_gradient", "section_left_block"};
    
    // 如果是全图背景布局,保持不变
    if ("section_full_image".equals(originalLayout)) {
        return originalLayout;
    }
    
    // 根据章节编号轮换使用不同布局
    int layoutIndex = (sectionNumber - 1) % sectionLayouts.length;
    return sectionLayouts[layoutIndex];
}
5.3.8 配图提示词生成

当页面需要配图时,系统会根据页面内容生成英文配图提示词:

java 复制代码
private String generateImagePromptWithDetails(String title, List<String> points,
                                    List<DetailedPoint> detailedPoints) {
    StringBuilder sb = new StringBuilder();
    sb.append("Professional business presentation slide about ");
    sb.append(cleanTextForPrompt(title));
    
    // 优先使用 detailedPoints 中的内容
    if (detailedPoints != null && !detailedPoints.isEmpty()) {
        sb.append(", featuring ");
        
        List<String> allContent = new ArrayList<>();
        for (DetailedPoint dp : detailedPoints) {
            if (dp.getPoint() != null) {
                allContent.add(cleanTextForPrompt(dp.getPoint()));
            }
            if (dp.getDetails() != null) {
                for (String detail : dp.getDetails()) {
                    allContent.add(cleanTextForPrompt(detail));
                }
            }
        }
        
        // 取前3个内容项,限制提示词长度
        int maxItems = Math.min(3, allContent.size());
        for (int i = 0; i < maxItems; i++) {
            if (i > 0) sb.append(", ");
            String content = allContent.get(i);
            if (content.length() > 50) {
                content = content.substring(0, 50);
            }
            sb.append(content);
        }
    }
    
    sb.append(", modern minimalist style, clean design, corporate colors, no text");
    return sb.toString();
}

5.4 PPT渲染实现

使用Apache POI库进行PPT文件的生成:

java 复制代码
// 创建PPT文档
XMLSlideShow ppt = new XMLSlideShow();

// 设置幻灯片尺寸(16:9)
ppt.setPageSize(new Dimension(1280, 720));

// 创建幻灯片
XSLFSlide slide = ppt.createSlide();

// 添加标题
XSLFTextBox titleBox = slide.createTextBox();
titleBox.setAnchor(new Rectangle(50, 50, 1180, 100));
XSLFTextParagraph titlePara = titleBox.addNewTextParagraph();
XSLFTextRun titleRun = titlePara.addNewTextRun();
titleRun.setText("幻灯片标题");
titleRun.setFontSize(36.0);
titleRun.setFontColor(Color.BLACK);

// 保存文件
ByteArrayOutputStream out = new ByteArrayOutputStream();
ppt.write(out);
byte[] pptBytes = out.toByteArray();

六、核心功能实现:播客生成

6.1 SoulX-Podcast 开源服务介绍

播客生成功能基于 SoulX-Podcast 开源项目实现,这是一个基于 Python 的语音克隆和多人对话生成服务。

6.1.1 服务特点
特性 说明
语音克隆 支持通过参考音频克隆说话人声音
多人对话 支持最多4个说话人的对话生成
异步处理 采用异步任务模式,适合长音频生成
高质量输出 支持24000Hz采样率的WAV格式输出
6.1.2 API接口规范

SoulX-Podcast 提供以下核心API接口:

1. 异步生成接口

http 复制代码
POST /generate-async
Content-Type: multipart/form-data

# 请求参数
prompt_audio: 参考音频文件(WAV格式,可多个)
prompt_texts: 角色描述JSON数组
dialogue_text: 对话文本(特定格式)
seed: 随机种子(可选,默认1988)
temperature: 温度参数(可选,默认0.6)
top_k: Top-K采样(可选,默认100)
top_p: Top-P采样(可选,默认0.9)
repetition_penalty: 重复惩罚(可选,默认1.25)

# 响应
{
  "task_id": "uuid-string",
  "status": "pending",
  "message": "Task submitted successfully"
}

2. 任务状态查询接口

http 复制代码
GET /task/{task_id}

# 响应
{
  "task_id": "uuid-string",
  "status": "completed|processing|failed",
  "progress": 75,
  "duration": 180,
  "result_url": "/download/output_xxx.wav",
  "error": null
}

3. 音频下载接口

http 复制代码
GET /download/{filename}

# 响应
Binary audio data (audio/wav)

4. 健康检查接口

http 复制代码
GET /health

# 响应
{
  "status": "healthy",
  "version": "1.0.0"
}
6.1.3 对话文本格式规范

SoulX-Podcast 使用特定的对话文本格式:

复制代码
[S1]说话人1的内容<|breathing|>[S2]说话人2的内容<|breathing|>[S1]说话人1继续说...

格式说明:

  • [S1][S2][S3][S4]:说话人标识符,最多支持4个说话人
  • <|breathing|>:停顿标记,在说话人切换处添加,产生自然的呼吸停顿效果
  • 整个对话是单行格式,不使用换行符分隔

示例:

复制代码
[S1]欢迎收听本期播客,今天我们来聊聊人工智能的发展。<|breathing|>[S2]是的,AI技术近年来发展非常迅速,特别是大语言模型的突破。<|breathing|>[S1]那您能给我们介绍一下大语言模型的核心原理吗?

6.2 播客生成架构

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                    CreativeWorkshopServiceImpl                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    generateAudioContent()                    │   │
│  │  1. 获取脚本JSON                                            │   │
│  │  2. 调用PodcastService                                      │   │
│  │  3. 更新任务状态                                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                       PodcastServiceImpl                             │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    generatePodcast()                         │   │
│  │  1. 转换对话格式                                            │   │
│  │  2. 生成角色描述                                            │   │
│  │  3. 提交异步任务                                            │   │
│  │  4. 轮询任务状态                                            │   │
│  │  5. 下载音频文件                                            │   │
│  │  6. 上传到S3                                                │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      SoulX-Podcast API (Python)                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  POST /generate-async    异步生成语音                        │   │
│  │  GET  /task/{task_id}    查询任务状态                        │   │
│  │  GET  /download/{file}   下载音频文件                        │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

6.3 对话格式转换

将脚本段落转换为SoulX-Podcast API所需的对话格式:

java 复制代码
/**
 * 格式: [S1]内容<|breathing|>[S2]内容...
 * 
 * 停顿标记说明:
 * - 在角色切换处添加 <|breathing|> 标记,产生自然的停顿效果
 */
private String convertToDialogueFormat(List<Map<String, Object>> segments, String dialect) {
    StringBuilder sb = new StringBuilder();
    Map<String, String> speakerMap = new HashMap<>();
    int speakerIndex = 1;
    String lastSpeakerId = null;
    
    for (Map<String, Object> segment : segments) {
        String speaker = (String) segment.get("speaker");
        String text = (String) segment.get("text");
        
        if (text == null || text.trim().isEmpty()) {
            continue;
        }
        
        // 分配说话人标识
        if (!speakerMap.containsKey(speaker)) {
            speakerMap.put(speaker, "S" + speakerIndex++);
        }
        String speakerId = speakerMap.get(speaker);
        
        // 在角色切换处添加停顿标记
        if (!speakerId.equals(lastSpeakerId)) {
            if (lastSpeakerId != null) {
                sb.append("<|breathing|>");
            }
            sb.append("[").append(speakerId).append("]");
            lastSpeakerId = speakerId;
        }
        sb.append(text.trim());
    }
    
    return sb.toString();
}

转换示例:

输入脚本:

json 复制代码
[
  {"speaker": "host", "text": "欢迎收听本期播客"},
  {"speaker": "guest", "text": "大家好,很高兴来到这里"},
  {"speaker": "host", "text": "今天我们来聊聊AI技术"}
]

输出对话格式:

复制代码
[S1]欢迎收听本期播客<|breathing|>[S2]大家好,很高兴来到这里<|breathing|>[S1]今天我们来聊聊AI技术

6.4 角色描述生成

根据音频风格生成合适的角色描述:

java 复制代码
private String generatePromptTexts(int speakerCount, String audioStyle) {
    List<String> promptTexts = new ArrayList<>();
    
    switch (audioStyle != null ? audioStyle.toLowerCase() : "default") {
        case "qa":
            // 问答风格 - 2人对话
            promptTexts.add("资深播客主持人,声音亲切自然,善于提问和引导话题。");
            if (speakerCount > 1) {
                promptTexts.add("专业嘉宾,表达清晰有见地,善于解答问题。");
            }
            break;
            
        case "debate":
            // 辩论风格 - 2人对话
            promptTexts.add("资深辩论主持人,声音有力量感,善于引导讨论。");
            if (speakerCount > 1) {
                promptTexts.add("专业辩手,逻辑清晰,善于表达不同观点。");
            }
            break;
            
        case "overview":
            // 概述风格 - 1人讲解
            promptTexts.add("专业讲解员,声音清晰有条理,善于概括和总结。");
            break;
            
        case "review":
            // 评价风格 - 1人讲解
            promptTexts.add("资深评论员,声音沉稳有深度,善于分析和评价。");
            break;
            
        default:
            promptTexts.add("专业播客主持人,声音清晰有感染力。");
            break;
    }
    
    return JSON.toJSONString(promptTexts);
}

6.5 异步任务提交

使用WebClient提交multipart/form-data请求到SoulX-Podcast服务:

6.5.1 请求构建
java 复制代码
private JSONObject submitAsyncTask(String dialogueText, String promptTexts) {
    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    
    // 检查对话中有几个说话人
    boolean hasMultipleSpeakers = dialogueText.contains("[S2]");
    
    // 加载参考音频
    byte[] promptAudioBytes1 = loadDefaultPromptAudio(1);
    builder.part("prompt_audio", new ByteArrayResource(promptAudioBytes1) {
        @Override
        public String getFilename() {
            return "prompt1.wav";
        }
    }).contentType(MediaType.parseMediaType("audio/wav"));
    
    // 如果有多个说话人,加载第二个参考音频
    if (hasMultipleSpeakers) {
        byte[] promptAudioBytes2 = loadDefaultPromptAudio(2);
        builder.part("prompt_audio", new ByteArrayResource(promptAudioBytes2) {
            @Override
            public String getFilename() {
                return "prompt2.wav";
            }
        }).contentType(MediaType.parseMediaType("audio/wav"));
    }
    
    // 添加其他参数
    builder.part("prompt_texts", promptTexts);
    builder.part("dialogue_text", dialogueText);
    builder.part("seed", "1988");
    builder.part("temperature", "0.6");
    builder.part("top_k", "100");
    builder.part("top_p", "0.9");
    builder.part("repetition_penalty", "1.25");
    
    // 发送请求
    String response = webClient.post()
            .uri("/generate-async")
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .body(BodyInserters.fromMultipartData(builder.build()))
            .retrieve()
            .bodyToMono(String.class)
            .timeout(Duration.ofSeconds(30))
            .block();
    
    return JSON.parseObject(response);
}
6.5.2 生成参数说明
参数 类型 默认值 说明
seed int 1988 随机种子,相同种子可复现结果
temperature float 0.6 控制生成的随机性,越高越随机
top_k int 100 Top-K采样,限制候选token数量
top_p float 0.9 Top-P采样,累积概率阈值
repetition_penalty float 1.25 重复惩罚,避免重复生成
6.5.3 参考音频要求
  • 格式:WAV格式,16位PCM编码
  • 采样率:建议24000Hz或44100Hz
  • 时长:建议5-15秒的清晰语音
  • 内容:包含说话人典型的语音特征
  • 数量:与说话人数量匹配(1-4个)

6.6 任务状态轮询

java 复制代码
private JSONObject pollTaskStatus(String podcastTaskId, String uid, 
                                   String taskId, ProgressCallback callback) {
    int attempts = 0;
    int consecutiveErrors = 0;
    final int MAX_CONSECUTIVE_ERRORS = 10;
    
    while (attempts < podcastConfig.getMaxPollingAttempts()) {
        try {
            Thread.sleep(podcastConfig.getPollingInterval());  // 默认5秒
            
            String response = webClient.get()
                    .uri("/task/{taskId}", podcastTaskId)
                    .retrieve()
                    .bodyToMono(String.class)
                    .timeout(Duration.ofSeconds(30))
                    .block();
            
            if (response == null) {
                consecutiveErrors++;
                if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
                    return null;  // 连续错误过多,放弃
                }
                continue;
            }
            
            consecutiveErrors = 0;
            JSONObject status = JSON.parseObject(response);
            
            // 回调进度
            if (callback != null) {
                Integer progress = status.getInteger("progress");
                String statusStr = status.getString("status");
                callback.onProgress(progress != null ? progress : 0, statusStr);
            }
            
            String statusStr = status.getString("status");
            if ("completed".equals(statusStr)) {
                return status;  // 任务完成
            }
            if ("failed".equals(statusStr)) {
                return status;  // 任务失败
            }
            
        } catch (Exception e) {
            consecutiveErrors++;
        }
        
        attempts++;
    }
    
    return null;  // 超时
}

6.7 音频下载与上传

java 复制代码
/**
 * 下载音频文件(带重试机制)
 */
private byte[] downloadAudioFile(String filename) {
    int maxRetries = 3;
    Exception lastException = null;
    
    for (int retry = 0; retry < maxRetries; retry++) {
        try {
            if (retry > 0) {
                log.info("下载Podcast音频第{}次重试: filename={}", retry + 1, filename);
                Thread.sleep(2000); // 重试前等待2秒
            }
            
            byte[] audioData = webClient.get()
                    .uri("/download/{filename}", filename)
                    .retrieve()
                    .bodyToMono(byte[].class)
                    .timeout(Duration.ofSeconds(120)) // 增加超时时间到120秒
                    .block();
            
            if (audioData != null && audioData.length > 0) {
                // 验证音频数据的基本完整性
                if (audioData.length < 1000) {
                    log.warn("下载的音频数据过小,可能不完整: filename={}, size={}bytes",
                            filename, audioData.length);
                }
                log.info("下载Podcast音频成功: filename={}, size={}bytes", filename, audioData.length);
                return audioData;
            }
        } catch (Exception e) {
            lastException = e;
            log.warn("下载Podcast音频第{}次尝试失败: filename={}, error={}",
                    retry + 1, filename, e.getMessage());
        }
    }
    
    log.error("下载Podcast音频失败(重试{}次后): filename={}", maxRetries, filename, lastException);
    return null;
}

6.8 服务配置说明

播客生成服务通过Spring Boot的配置属性进行管理,主要配置项包括:

配置项 说明 建议值
服务地址 SoulX-Podcast服务的内网地址 根据部署环境配置
启用开关 是否启用播客服务 true/false
轮询间隔 异步任务状态轮询间隔 5000ms
最大轮询次数 防止无限等待的上限 720次(约1小时)
请求超时 HTTP请求超时时间 30秒
音频格式 输出音频格式 wav
采样率 音频采样率 24000Hz
参考音频 语音克隆的参考音频文件 S3存储路径

配置类定义:

java 复制代码
@Data
@Configuration
@ConfigurationProperties(prefix = "podcast")
public class PodcastConfig {
    private String baseUrl;           // 服务地址
    private boolean enabled;          // 启用开关
    private long pollingInterval;     // 轮询间隔
    private int maxPollingAttempts;   // 最大轮询次数
    private int requestTimeout;       // 请求超时
    private String audioFormat;       // 音频格式
    private int sampleRate;           // 采样率
    private String defaultPromptAudio;  // 参考音频1
    private String defaultPromptAudio2; // 参考音频2
}

注意:具体的服务地址、S3存储路径等敏感配置信息应通过环境变量或配置中心管理,不应硬编码在代码或配置文件中。


七、AI提示词工程

7.1 概览生成提示词

java 复制代码
private String buildSummaryPrompt(String title, String content) {
    StringBuilder sb = new StringBuilder();
    
    sb.append("你是一个专业的内容分析助手。请根据以下内容生成概览和推荐问题。\n\n");
    sb.append("【原始内容】\n").append(content).append("\n\n");
    
    // 检测是否有多个来源
    boolean hasMultipleSources = content.contains("【来源概览】");
    if (hasMultipleSources) {
        sb.append("【多来源均衡覆盖要求 - 极其重要】\n");
        sb.append("1. 【均衡性】概览必须涵盖所有来源的核心内容\n");
        sb.append("2. 【完整性】每个来源的主要观点都应该在概览中有所体现\n");
        sb.append("3. 【融合性】将多个来源的内容有机整合\n");
        sb.append("4. 【检查】生成后请自我检查:是否每个来源都被提及?\n\n");
    }

    sb.append("【输出要求】\n");
    sb.append("请生成以下内容:\n");
    sb.append("1. icon: 一个最能代表这个内容主题的emoji表情\n");
    sb.append("2. title: 一个简洁有力的标题(5-15字)\n");
    sb.append("3. summary: 内容概览(100-300字)\n");
    sb.append("4. suggestedQuestions: 3个推荐问题\n\n");
    
    sb.append("请以JSON格式返回:\n");
    sb.append("{\n");
    sb.append("  \"icon\": \"📚\",\n");
    sb.append("  \"title\": \"内容标题\",\n");
    sb.append("  \"summary\": \"内容概览...\",\n");
    sb.append("  \"suggestedQuestions\": [\"问题1?\", \"问题2?\", \"问题3?\"]\n");
    sb.append("}\n");
    
    return sb.toString();
}

7.2 音频大纲生成提示词

针对不同的音频风格,生成差异化的大纲结构:

7.2.1 问答风格(qa)
java 复制代码
sb.append("🎤【输出要求 - 问答访谈风格】\n");
sb.append("请生成一个【问答访谈】形式的音频大纲,模拟主持人与专家嘉宾的深度对话。\n\n");

sb.append("⚠️【问答风格核心特征 - 必须严格遵守】\n");
sb.append("1. 【双人对话】必须是主持人和嘉宾两个角色的对话形式\n");
sb.append("2. 【问题驱动】每个章节标题必须是疑问句形式(以?结尾)\n");
sb.append("3. 【互动性强】主持人提问→嘉宾回答→主持人追问/总结\n");
sb.append("4. 【由浅入深】问题从基础概念到深入分析,循序渐进\n");
sb.append("5. 【教育导向】目的是让听众通过问答学习知识\n\n");

sb.append("【章节标题格式验证 - 极其重要】\n");
sb.append("✅ 正确格式(必须是问句):\n");
sb.append("   - \"什么是人工智能?\"\n");
sb.append("   - \"如何入门机器学习?\"\n");
sb.append("❌ 错误格式(禁止使用陈述句):\n");
sb.append("   - \"人工智能概述\" ← 错误!必须改为问句\n");
7.2.2 辩论风格(debate)
java 复制代码
sb.append("⚔️【输出要求 - 辩论讨论风格】\n");
sb.append("请生成一个【辩论讨论】形式的音频大纲,模拟两位专家的观点交锋。\n\n");

sb.append("⚠️【辩论风格核心特征 - 必须严格遵守】\n");
sb.append("1. 【双方对立】必须有正方和反方两个明确的立场\n");
sb.append("2. 【观点交锋】每个章节都要有观点的碰撞和讨论\n");
sb.append("3. 【论据支撑】每个观点都要有具体的论据和案例\n");
sb.append("4. 【逻辑严密】论证过程要有逻辑性\n");
sb.append("5. 【开放结论】不强制得出统一结论,允许保留分歧\n\n");
7.2.3 概述风格(overview)
java 复制代码
sb.append("📖【输出要求 - 概述讲解风格】\n");
sb.append("请生成一个【概述讲解】形式的音频大纲,由一位专业讲解员清晰概括内容。\n\n");

sb.append("⚠️【概述风格核心特征 - 必须严格遵守】\n");
sb.append("1. 【单人讲解】只有一位讲解员,不需要对话\n");
sb.append("2. 【结构清晰】内容按逻辑顺序组织,层次分明\n");
sb.append("3. 【重点突出】突出核心要点,避免冗余\n");
sb.append("4. 【通俗易懂】语言简洁明了,易于理解\n");
sb.append("5. 【总结归纳】每个章节结束时有简短总结\n\n");

7.3 音频脚本生成提示词

java 复制代码
private String buildAudioScriptPrompt(String audioStyle, String voicePreference,
                                       String content, Map<String, Object> outline) {
    StringBuilder sb = new StringBuilder();
    
    // 确定语言风格
    boolean isProfessional = "professional".equals(voicePreference);
    String toneDescription = isProfessional
        ? "专业严谨、学术化、使用专业术语、逻辑清晰"
        : "轻松通俗、口语化、易于理解、生动有趣";
    
    sb.append("你是一个专业的播客脚本撰写专家。\n\n");
    sb.append("【语言风格】").append(toneDescription).append("\n\n");
    
    sb.append("【大纲结构】\n").append(JSON.toJSONString(outline)).append("\n\n");
    sb.append("【原始内容】\n").append(content).append("\n\n");
    
    // 根据音频风格添加特定要求
    switch (audioStyle) {
        case "qa":
            sb.append("【问答脚本要求】\n");
            sb.append("1. 主持人用[S1]标记,嘉宾用[S2]标记\n");
            sb.append("2. 主持人负责提问和引导,嘉宾负责回答和解释\n");
            sb.append("3. 对话要自然流畅,有互动感\n");
            sb.append("4. 可以添加情感标记如[笑]、[思考]等\n");
            break;
            
        case "overview":
            sb.append("【概述脚本要求】\n");
            sb.append("1. 只使用[S1]标记(单人讲解)\n");
            sb.append("2. 语言要清晰、有条理\n");
            sb.append("3. 适当使用过渡语句连接各部分\n");
            sb.append("4. 每个章节结束时简短总结\n");
            break;
    }
    
    sb.append("\n【输出格式】\n");
    sb.append("返回JSON数组,每个元素包含:\n");
    sb.append("- speaker: 说话人标识(host/guest)\n");
    sb.append("- text: 说话内容\n");
    sb.append("- emotion: 情感标记(可选)\n");
    
    return sb.toString();
}

7.4 PPT大纲生成提示词

java 复制代码
private String buildOutlinePrompt(String mediaType, String title, String content, 
                                   String pptStyle, String audioStyle, String voicePreference) {
    StringBuilder sb = new StringBuilder();
    
    // 128k上下文模型,content限制调整为60000字符
    final int MAX_CONTENT_LENGTH = 60000;
    
    sb.append("你是一个专业的内容创作助手。请根据以下内容生成一个结构化的大纲。\n\n");
    sb.append("【内容标题】\n").append(title).append("\n\n");
    sb.append("【原始内容】\n").append(
        content.length() > MAX_CONTENT_LENGTH ? content.substring(0, MAX_CONTENT_LENGTH) : content
    ).append("\n\n");
    
    // 多来源均衡覆盖要求
    boolean hasMultipleSources = content.contains("【来源概览】");
    if (hasMultipleSources) {
        sb.append("【多来源均衡覆盖要求 - 极其重要】\n");
        sb.append("1. 【均衡性】每个来源的内容在大纲中的占比应该大致相当\n");
        sb.append("2. 【完整性】每个来源的核心要点都必须在大纲中有所体现\n");
        sb.append("3. 【融合性】如果多个来源有相关联的内容,可以进行有机整合\n");
        sb.append("4. 【检查】生成完成后,请自我检查:是否每个来源都有足够的内容体现?\n\n");
    }
    
    // 根据PPT风格调整详细度
    boolean isSimple = "simple".equalsIgnoreCase(pptStyle);
    
    sb.append("【输出要求】\n");
    if (isSimple) {
        sb.append("请生成一个简洁精炼的PPT演示文稿大纲,要求结构清晰、重点突出。\n\n");
        sb.append("【结构要求】\n");
        sb.append("1. 3-5个主要章节(精简核心内容)\n");
        sb.append("2. 每个章节2-3个要点\n");
    } else {
        sb.append("请生成一个专业、内容丰富的PPT演示文稿大纲。\n\n");
        sb.append("【结构要求】\n");
        sb.append("1. 6-10个主要章节(根据内容深度调整)\n");
        sb.append("2. 每个章节4-6个要点\n");
        sb.append("3. 每个要点对应的详细说明(60-120字)\n");
    }
    
    return sb.toString();
}

八、SSE流式响应实现

8.1 SSE基础概念

Server-Sent Events (SSE) 是一种服务器向客户端推送数据的技术,特点:

  • 单向通信:服务器到客户端
  • 自动重连:连接断开后自动重连
  • 文本协议:基于HTTP,数据格式简单
  • 浏览器原生支持:无需额外库

8.2 Spring Boot SSE实现

8.2.1 创建SseEmitter
java 复制代码
@PostMapping(value = "/{workshopId}/script/generate", 
             produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter generateScript(@PathVariable Long workshopId,
                                  @RequestBody ScriptGenerateParam param) {
    String uid = SessionLocal.getLoginUser().getUid();
    return workshopService.generateScript(uid, workshopId, param);
}
8.2.2 服务层实现
java 复制代码
@Override
public SseEmitter generateScript(String uid, Long workshopId, ScriptGenerateParam param) {
    // 创建SseEmitter,设置超时时间
    SseEmitter emitter = new SseEmitter(1800000L); // 30分钟超时
    
    // 异步执行生成任务
    executorService.execute(() -> {
        try {
            // 发送进度
            sendSseStatus(emitter, workshopId, "outline", "正在分析来源内容...", 5);
            
            // 生成大纲
            Map<String, Object> outline = generateOutlineWithAI(...);
            sendSseStatus(emitter, workshopId, "outline", "大纲生成完成", 40);
            
            // 流式输出大纲Markdown
            String outlineMarkdown = generateOutlineMarkdown(outline, mediaType);
            sendSseContent(emitter, workshopId, outlineMarkdown);
            
            // 生成脚本(如果需要)
            if (needsScript) {
                sendSseStatus(emitter, workshopId, "script", "正在生成脚本...", 60);
                List<Map<String, Object>> segments = generateScriptWithAI(...);
                sendSseStatus(emitter, workshopId, "script", "脚本生成完成", 100);
            }
            
            // 发送完成事件
            sendSseComplete(emitter, workshopId, scriptId, segments);
            emitter.send(SseEmitter.event().data("[DONE]"));
            emitter.complete();
            
        } catch (Exception e) {
            log.error("生成脚本失败", e);
            sendSseError(emitter, workshopId, "script", "生成失败: " + e.getMessage(), "WORKSHOP_500");
        }
    });
    
    return emitter;
}

8.3 SSE事件类型

8.3.1 状态事件(status)
java 复制代码
private void sendSseStatus(SseEmitter emitter, Long id, String phase, 
                           String message, int progress) throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", id);
    data.put("type", "status");
    data.put("phase", phase);      // outline / script
    data.put("message", message);
    data.put("progress", progress);
    emitter.send(SseEmitter.event().data(JSON.toJSONString(data)));
}
8.3.2 内容事件(content)
java 复制代码
private void sendSseContent(SseEmitter emitter, Long id, String text) throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", id);
    data.put("type", "content");
    data.put("text", text);
    emitter.send(SseEmitter.event().data(JSON.toJSONString(data)));
}
8.3.3 完成事件(complete)
java 复制代码
private void sendSseComplete(SseEmitter emitter, Long id, String scriptId,
                              List<Map<String, Object>> segments) throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", id);
    data.put("type", "complete");
    data.put("scriptId", scriptId);
    data.put("segments", segments);
    emitter.send(SseEmitter.event().data(JSON.toJSONString(data)));
}
8.3.4 错误事件(error)
java 复制代码
private void sendSseError(SseEmitter emitter, Long id, String phase, 
                          String message, String errorCode) throws Exception {
    Map<String, Object> data = new HashMap<>();
    data.put("id", id);
    data.put("type", "error");
    data.put("phase", phase);
    data.put("message", message);
    data.put("errorCode", errorCode);
    emitter.send(SseEmitter.event().data(JSON.toJSONString(data)));
    emitter.send(SseEmitter.event().data("[DONE]"));
    emitter.complete();
}

8.4 前端SSE处理

javascript 复制代码
// Flutter/Dart 示例
final client = http.Client();
final request = http.Request('POST', Uri.parse('$baseUrl/workshop/$workshopId/script/generate'));
request.headers['Content-Type'] = 'application/json';
request.headers['Accept'] = 'text/event-stream';
request.body = jsonEncode(param);

final response = await client.send(request);
final stream = response.stream.transform(utf8.decoder);

await for (final chunk in stream) {
  final lines = chunk.split('\n');
  for (final line in lines) {
    if (line.startsWith('data: ')) {
      final data = line.substring(6);
      if (data == '[DONE]') {
        // 完成
        break;
      }
      final json = jsonDecode(data);
      switch (json['type']) {
        case 'status':
          // 更新进度
          onProgress(json['progress'], json['message']);
          break;
        case 'content':
          // 显示内容
          onContent(json['text']);
          break;
        case 'complete':
          // 完成处理
          onComplete(json['scriptId'], json['segments']);
          break;
        case 'error':
          // 错误处理
          onError(json['message']);
          break;
      }
    }
  }
}

九、错误处理与重试机制

9.1 重试配置

java 复制代码
// 重试配置
private static final int MAX_RETRY_COUNT = 3;      // 最大重试次数
private static final long RETRY_DELAY_MS = 2000;   // 重试延迟(毫秒)

9.2 带重试的播客生成

java 复制代码
@Override
public PodcastResult generatePodcast(List<Map<String, Object>> segments,
                                      String dialect, String title,
                                      String uid, String taskId,
                                      String audioStyle,
                                      ProgressCallback progressCallback) {
    Exception lastException = null;
    String lastError = null;
    
    for (int retry = 0; retry < MAX_RETRY_COUNT; retry++) {
        try {
            if (retry > 0) {
                log.info("[uid:{}] Podcast生成第{}次重试, taskId: {}", uid, retry + 1, taskId);
                if (progressCallback != null) {
                    progressCallback.onProgress(0, "重试中 (" + (retry + 1) + "/" + MAX_RETRY_COUNT + ")...");
                }
                Thread.sleep(RETRY_DELAY_MS);
            }
            
            PodcastResult result = doGeneratePodcast(segments, dialect, title, 
                                                      uid, taskId, audioStyle, progressCallback);
            
            if (result.isSuccess()) {
                return result;
            } else {
                lastError = result.getErrorMessage();
                log.warn("[uid:{}] Podcast生成第{}次尝试失败, error: {}", uid, retry + 1, lastError);
                
                // 发送飞书告警(非最后一次重试)
                if (retry < MAX_RETRY_COUNT - 1) {
                    sendFeiShuRetryAlert(uid, taskId, lastError, retry + 1, MAX_RETRY_COUNT);
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return PodcastResult.failure("生成被中断");
        } catch (Exception e) {
            lastException = e;
            lastError = e.getMessage();
            log.error("[uid:{}] Podcast生成第{}次尝试异常", uid, retry + 1, e);
            
            if (retry < MAX_RETRY_COUNT - 1) {
                sendFeiShuRetryAlert(uid, taskId, e.getMessage(), retry + 1, MAX_RETRY_COUNT);
            }
        }
    }
    
    // 所有重试都失败,发送最终失败告警
    sendFeiShuFinalFailureAlert(uid, taskId, lastError);
    return PodcastResult.failure(lastError);
}

9.3 飞书告警集成

java 复制代码
// 飞书告警消息模板
private static final String FEISHU_ALERT_TEMPLATE = "【创意工坊告警】\n" +
        "任务类型: %s\n" +
        "任务ID: %s\n" +
        "用户ID: %s\n" +
        "错误信息: %s\n" +
        "重试次数: %d/%d\n" +
        "时间: %s";

/**
 * 发送飞书重试告警
 */
private void sendFeiShuRetryAlert(String uid, String taskId, String error, 
                                   int retryCount, int maxRetry) {
    String message = String.format(
        "⚠️ Podcast生成重试告警\n" +
        "用户ID: %s\n" +
        "任务ID: %s\n" +
        "重试次数: %d/%d\n" +
        "错误信息: %s",
        uid, taskId, retryCount, maxRetry, error
    );
    feiShuService.sendMessageToRobotAsync(message);
}

/**
 * 发送飞书最终失败告警
 */
private void sendFeiShuFinalFailureAlert(String uid, String taskId, String error) {
    String message = String.format(
        "❌ Podcast生成最终失败\n" +
        "用户ID: %s\n" +
        "任务ID: %s\n" +
        "错误信息: %s\n" +
        "已重试 %d 次均失败",
        uid, taskId, error, MAX_RETRY_COUNT
    );
    feiShuService.sendMessageToRobotAsync(message);
}

9.4 任务状态管理

java 复制代码
@Override
public SseEmitter createTask(String uid, Long workshopId, TaskCreateParam param) {
    CreativeTask task = taskMapper.selectByTaskId(param.getScriptId());
    
    // 检查任务状态,防止重复触发
    if (WorkshopTaskStatusEnum.PROCESSING.getCode().equals(task.getStatus())) {
        throw new ServiceException("-1", "该任务正在生成中,请勿重复操作");
    }
    
    final String taskId = param.getScriptId();
    SseEmitter emitter = new SseEmitter(1800000L);
    
    executorService.execute(() -> {
        try {
            // 双重检查,防止并发
            CreativeTask latestTask = taskMapper.selectByTaskId(taskId);
            if (WorkshopTaskStatusEnum.PROCESSING.getCode().equals(latestTask.getStatus())) {
                sendSseError(emitter, workshopId, "task", "任务正在生成中", "-1");
                return;
            }
            
            // 更新任务状态为处理中
            taskMapper.updateStatusAndProgress(taskId, WorkshopTaskStatusEnum.PROCESSING.getCode(), 0);
            
            // 执行生成任务...
            
            // 更新任务完成
            taskMapper.updateComplete(taskId, resultFileKey, duration, slideCount);
            
        } catch (Exception e) {
            log.error("创建任务失败, taskId: {}", taskId, e);
            
            // 确保异常时更新任务状态为失败
            try {
                taskMapper.updateFailed(taskId, e.getMessage());
            } catch (Exception updateEx) {
                log.error("更新任务失败状态异常", updateEx);
            }
            
            // 发送飞书告警
            sendFeiShuFinalFailureAlert("任务创建", taskId, uid, e.getMessage());
            
            // 发送错误事件
            sendSseError(emitter, workshopId, "task", "生成失败: " + e.getMessage(), "WORKSHOP_500");
        }
    });
    
    return emitter;
}

9.5 音频数据完整性验证

java 复制代码
// 验证音频数据完整性
// WAV 格式:每秒约 176KB (44100Hz * 16bit * 2channels / 8 / 1024)
// 预期最小大小:duration * 100KB(考虑压缩和格式差异)
long expectedMinSize = duration != null ? duration * 100 * 1024L : 0;
if (duration != null && audioData.length < expectedMinSize) {
    log.warn("[uid:{}] Podcast音频数据可能不完整: " +
            "actualSize={}bytes, expectedMinSize={}bytes, duration={}秒",
            uid, audioData.length, expectedMinSize, duration);
    
    // 发送飞书告警
    feiShuService.sendMessageToRobotAsync(String.format(
        "⚠️ Podcast音频可能不完整\n用户ID: %s\n任务ID: %s\n" +
        "实际大小: %d bytes\n预期最小: %d bytes\n时长: %d秒",
        uid, taskId, audioData.length, expectedMinSize, duration));
}

十、总结与最佳实践

10.1 架构设计要点

  1. 分层架构:Controller → Service → External Service → Data Layer,职责清晰
  2. 异步处理:使用ExecutorService处理耗时任务,避免阻塞主线程
  3. SSE流式响应:实时推送进度,提升用户体验
  4. 重试机制:关键操作支持自动重试,提高系统可靠性
  5. 告警集成:异常情况及时通知,便于问题排查

10.2 AI提示词工程要点

  1. 结构化输出:要求AI返回JSON格式,便于解析
  2. 多来源均衡:检测多来源内容,要求AI均衡覆盖
  3. 风格差异化:根据不同风格生成差异化的提示词
  4. 内容完整性:强调不能遗漏或简化内容
  5. 自我检查:要求AI生成后自我检查内容完整性

10.3 性能优化建议

  1. 内容长度限制:128k上下文模型,内容限制在60000字符
  2. 本地缓存:参考音频使用本地文件缓存,减少S3访问
  3. 连接池复用:WebClient配置连接池,复用HTTP连接
  4. 异步上传:生成完成后异步上传到S3

10.4 错误处理最佳实践

  1. 双重检查:防止并发导致的重复执行
  2. 状态管理:任务状态及时更新,避免卡在"处理中"
  3. 优雅降级:AI生成失败时回退到规则模式
  4. 日志分级:网络偶发错误使用DEBUG级别,避免日志过多
  5. 告警分级:重试告警和最终失败告警分开处理

10.5 扩展性设计

  1. 媒体类型枚举:新增媒体类型只需添加枚举值
  2. 布局类型扩展:PPT布局类型可灵活扩展
  3. 音频风格扩展:支持添加新的音频风格
  4. 外部服务抽象:Podcast服务通过接口抽象,便于替换实现

相关推荐
刘贺同学4 分钟前
Day12-龙虾哥打工日记:OpenClaw 子 Agent 到底看到了什么?
aigc·ai编程
程序员鱼皮2 小时前
离大谱,我竟然在 VS Code 里做了个视频!
github·aigc·ai编程
Kayshen4 小时前
我用纯前端逆向了 Figma 的二进制文件格式,实现了 .fig 文件的完整解析和导入
前端·agent·ai编程
wangruofeng4 小时前
OpenClaw 飞书机器人不回复消息?3 小时踩坑总结
ai编程
恋猫de小郭5 小时前
AI 正在造就你的「认知卸载」,但是时代如此
前端·人工智能·ai编程
草梅友仁6 小时前
墨梅博客 1.7.0 发布与 AI 开发实践 | 2026 年第 9 周草梅周报
开源·github·ai编程
孟健16 小时前
我用OpenClaw搭了11个AI Agent,它们学会了自我进化
agent·ai编程·claude
孟健16 小时前
Vibe Coding 的尽头是 AI Agent 军团:我用 16 个 Agent 自动化了整个创业公司
agent·ai编程·claude
潘高16 小时前
一口气搞懂AI热词:从LLM到Agent,你不再当小白!
ai编程