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服务通过接口抽象,便于替换实现

相关推荐
一见2 小时前
Skills、Rules和KnowledgeBase的概念和区别
人工智能·ai编程
楠奕2 小时前
0代码打造APP
ai编程
古城小栈2 小时前
后端接入大模型实现“自然语言查数据库”
数据库·ai编程
Sammyyyyy2 小时前
Gemini CLI 进阶:构建安全的MCP连接与验证策略
开发语言·ai·ai编程·servbay
HyperAI超神经3 小时前
揭秘 AI 推理:OpenAI 稀疏模型让神经网络首次透明化;Calories Burnt Prediction:为健身模型注入精准能量数据
人工智能·深度学习·神经网络·机器学习·开源·ai编程
mCell8 小时前
10分钟复刻爆火「死了么」App:vibe coding 实战(Expo+Supabase+MCP)
react native·ai编程·vibecoding
jacky25712 小时前
衍射光波导与阵列光波导技术方案研究
aigc·ar·xr·ai编程·仿真·混合现实·光学设计
人工智能训练15 小时前
UE5 如何显示蓝图运行流程
人工智能·ue5·ai编程·数字人·蓝图
deephub15 小时前
构建自己的AI编程助手:基于RAG的上下文感知实现方案
人工智能·机器学习·ai编程·rag·ai编程助手