AI演讲稿生成与PPT联动

从文本脚本到幻灯片同步的端到端管线


1. 问题定义

传统的 PPT 制作流程中,"写稿"和"做幻灯片"是两个独立环节,往往由不同的人完成,导致以下问题:

  • 稿子说的和幻灯片展示的不一致
  • 某页幻灯片内容太多,演讲者无法在合理时间内讲完
  • 幻灯片翻页节奏与演讲节奏不同步
  • 视觉元素(动画、高亮)无法与演讲要点配合

本文探讨如何用 AI 实现"演讲稿与幻灯片联动生成",使两者从一开始就保持结构同步。

传统流程:
写稿 ----独立----> 做 PPT ----手动对齐----> 排练
   (各做各的)              (费时费力)

联动流程:
意图分析 --> 大纲 --> 稿件+幻灯片并行生成 --> 自动同步 --> 排练
               |         |                       |
               +-- 共享结构 (Section/Beat) --------+

2. 系统架构

2.1 双轨生成模型

核心思路是"共享大纲,双轨生成":大纲是单一事实源,演讲稿和幻灯片从同一份大纲中分别生成,通过 Section ID 保持对齐。

用户意图
    |
    v
+------------------+
|  大纲生成引擎     |
|  (共享结构)       |
+------------------+
    |
    +---> [演讲稿轨道]
    |       |
    |       v
    |     Section 1 -> 演讲文本 + 时间估算
    |     Section 2 -> 演讲文本 + 时间估算
    |     ...
    |
    +---> [幻灯片轨道]
            |
            v
          Section 1 -> 幻灯片页面 + 动画序列
          Section 2 -> 幻灯片页面 + 动画序列
          ...

2.2 数据模型

// 共享大纲
interface PresentationOutline {
  title: string;
  total_duration_minutes: number;
  audience: AudienceProfile;
  sections: Section[];
}

interface Section {
  id: string;                    // "section_01"
  title: string;
  type: SectionType;             // opening / content / transition / closing
  beats: Beat[];                 // 每个 beat 是一个叙事单元
  target_duration_seconds: number;
}

interface Beat {
  id: string;                    // "section_01_beat_02"
  intent: string;                // "展示用户增长数据并强调拐点"
  key_message: string;           // "7月新功能上线后用户增长翻倍"
  data_refs?: string[];          // 关联数据源
  visual_hint?: string;          // "折线图+标注"
  estimated_seconds: number;
}

// 演讲稿
interface SpeechScript {
  sections: SpeechSection[];
  total_word_count: number;
  estimated_duration_seconds: number;
}

interface SpeechSection {
  section_id: string;            // 对应 Section.id
  beats: SpeechBeat[];
}

interface SpeechBeat {
  beat_id: string;               // 对应 Beat.id
  text: string;                  // 演讲文本
  word_count: number;
  estimated_seconds: number;
  notes?: string;                // 演讲者提示(语气、停顿、手势)
  cue: SlideCue;                 // 幻灯片同步指令
}

interface SlideCue {
  action: 'next_slide' | 'next_animation' | 'none';
  timing: 'before_text' | 'after_text' | 'at_keyword';
  keyword?: string;              // 当 timing='at_keyword' 时
}

// 幻灯片
interface SlideDefinition {
  section_id: string;
  slide_index: number;
  beats_covered: string[];       // 当前页覆盖哪些 beat
  elements: SlideElement[];
  animations: AnimationSequence[];
}

3. 演讲稿生成引擎

3.1 生成管线

大纲 (Outline)
    |
    v
[Section 分配] -- 根据 target_duration 分配字数预算
    |
    v
[Beat 展开] -- 每个 beat 生成 1-3 句演讲文本
    |
    v
[衔接润色] -- 添加 section 之间的过渡句
    |
    v
[时间校准] -- 按语速估算时间,调整字数
    |
    v
[Cue 标注] -- 标记翻页/动画触发点
    |
    v
最终演讲稿

3.2 字数与时间估算

中文演讲的语速参考:

场景 语速(字/分钟) 适用情况
慢速 120-150 正式致辞、学术报告
中速 150-200 商务汇报、教学
快速 200-250 产品演示、激情演讲
TED 风格 170-190 通用公开演讲
def estimate_speech_time(
    text: str,
    pace: str = 'medium',
    include_pauses: bool = True
) -> float:
    """估算演讲时间(秒)"""

    pace_map = {
        'slow': 140,
        'medium': 175,
        'fast': 220,
    }
    chars_per_minute = pace_map[pace]

    # 中文字数(去除标点和空格)
    chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
    # 英文单词数
    english_words = len(re.findall(r'[a-zA-Z]+', text))
    # 数字组
    numbers = len(re.findall(r'\d+', text))

    # 中文按字算,英文按词算(一个词约等于 1.5 个中文字的时间)
    equivalent_chars = chinese_chars + english_words * 1.5 + numbers * 1.0

    base_seconds = (equivalent_chars / chars_per_minute) * 60

    if include_pauses:
        # 每个句号/感叹号后停顿 0.5s
        sentence_breaks = len(re.findall(r'[。!?]', text))
        # 每个逗号/分号后停顿 0.2s
        clause_breaks = len(re.findall(r'[,;、]', text))
        # 段落间停顿 1.0s
        paragraph_breaks = text.count('\n\n')

        pause_seconds = (
            sentence_breaks * 0.5
            + clause_breaks * 0.2
            + paragraph_breaks * 1.0
        )
        base_seconds += pause_seconds

    return round(base_seconds, 1)

3.3 LLM 演讲稿生成 Prompt

def build_speech_prompt(section: Section, context: dict) -> str:
    return f"""你是一位专业的演讲稿撰写者。根据以下大纲片段生成演讲文本。

## 上下文
- 演讲主题: {context['title']}
- 受众: {context['audience']}
- 风格: {context['style']}
- 本节时长目标: {section.target_duration_seconds} 秒
- 语速: {context['pace']}(约 {context['chars_per_minute']} 字/分钟)
- 本节字数预算: {section.word_budget} 字

## 本节大纲
标题: {section.title}
类型: {section.type}

## 叙事要点(Beat)
{format_beats(section.beats)}

## 要求
1. 每个 beat 生成 1-3 句自然流畅的口语化文本
2. 在 beat 之间添加自然的过渡
3. 总字数控制在 {section.word_budget} 字左右(误差 10% 以内)
4. 在需要翻页或触发动画的位置标注 [SLIDE_CUE: next_slide] 或 [SLIDE_CUE: next_animation]
5. 在需要特殊语气的地方标注 [NOTE: 停顿/强调/降低声音]

## 输出格式
按 beat_id 分段输出,每段包含 beat_id、演讲文本、时间估算。
"""

4. 幻灯片联动生成

4.1 Beat 到 Slide 映射

一个 beat 不一定对应一页幻灯片。映射规则:

Beat 特征 映射策略 说明
独立数据展示 1 beat = 1 slide 数据图表需要独占空间
连续要点 N beats = 1 slide 3-5 个要点合并到一页,用动画逐条展示
过渡 beat 附属于前/后 slide 过渡语不需要独立页面
复杂论证 1 beat = N slides 拆分为多页逐步展开
def map_beats_to_slides(beats: list[Beat]) -> list[SlideMapping]:
    """将 beats 映射为 slides"""
    slides = []
    current_group = []

    for beat in beats:
        if beat.visual_hint and 'chart' in beat.visual_hint:
            # 有独立图表的 beat 独占一页
            if current_group:
                slides.append(SlideMapping(
                    beats=current_group,
                    layout='bullet_list',
                    animation='sequential'
                ))
                current_group = []
            slides.append(SlideMapping(
                beats=[beat],
                layout='data_chart',
                animation='chart_reveal'
            ))

        elif beat.intent.startswith('过渡') or beat.intent.startswith('transition'):
            # 过渡 beat 附属于前一页
            if slides:
                slides[-1].transition_text = beat.key_message
            continue

        else:
            current_group.append(beat)
            if len(current_group) >= 4:
                # 累积到 4 个要点就生成一页
                slides.append(SlideMapping(
                    beats=current_group,
                    layout='bullet_list',
                    animation='sequential'
                ))
                current_group = []

    if current_group:
        slides.append(SlideMapping(
            beats=current_group,
            layout='bullet_list',
            animation='sequential'
        ))

    return slides

4.2 动画与演讲同步

演讲时间轴:
|----Beat 1----|----Beat 2----|----Beat 3----|
0s            5s            10s            15s

幻灯片时间轴:
|--翻页--|--动画1--|--动画2--|--动画3--|
0s      0.5s     5.5s     10.5s

同步事件:
t=0s    : [SLIDE_CUE: next_slide] 翻到新页
t=0.5s  : 第一条要点淡入(自动)
t=5s    : [SLIDE_CUE: next_animation] 演讲者讲到 Beat 2 时
t=5.5s  : 第二条要点淡入
t=10s   : [SLIDE_CUE: next_animation] 演讲者讲到 Beat 3 时
t=10.5s : 第三条要点淡入

5. 演讲者视图

5.1 信息布局

+------------------------------------------------------------------+
|                      演讲者视图                                    |
+------------------------------------------------------------------+
|                           |                                       |
|   [当前幻灯片预览]        |   [演讲稿文本]                        |
|   +-----------------+     |   当前 Beat (高亮):                   |
|   |                 |     |   "接下来我们看一下用户增长的         |
|   |   当前页面      |     |    关键数据。从这张图中可以看到..."    |
|   |                 |     |                                       |
|   +-----------------+     |   下一 Beat (灰色):                   |
|                           |   "值得注意的是,7月份之后..."        |
|   [下一页预览]             |                                       |
|   +--------+              |   [NOTE: 在此处停顿 2 秒]             |
|   |  缩略  |              |                                       |
|   +--------+              +---------------------------------------+
|                           |                                       |
+---------------------------+   时间: 05:23 / 20:00                 |
|   进度: =====>-----       |   语速: 178 字/分 (正常)              |
|   第 8/25 页               |   剩余预算: 14:37                    |
+------------------------------------------------------------------+

5.2 实时反馈

指标 正常范围 警告条件 提示方式
语速 150-200 字/分 < 120 或 > 230 速度指示条变色
时间进度 偏差 < 10% 偏差 > 20% 进度条变红
当前页停留 30-120s > 180s 提示"时间过长"
剩余页数 vs 剩余时间 匹配 严重不匹配 建议跳过某些页

6. 排练与优化循环

6.1 排练模式

+-----------+     +----------------+     +-------------+
| 排练录音  | --> | 语音转文字     | --> | 对比分析    |
| (实际演讲)|     | (Whisper/STT)  |     | (稿件 vs   |
|           |     |                |     |  实际文本)  |
+-----------+     +----------------+     +-------------+
                                               |
                                               v
                                        +-------------+
                                        | 优化建议    |
                                        | - 跳过的内容 |
                                        | - 超时的部分 |
                                        | - 口头禅统计 |
                                        +-------------+

6.2 优化建议引擎

def analyze_rehearsal(
    script: SpeechScript,
    recording_transcript: str,
    recording_duration: float
) -> RehearsalReport:
    """对比原稿与排练录音,生成优化建议"""

    # 文本对齐(DTW 算法)
    alignment = align_texts(script.full_text, recording_transcript)

    suggestions = []

    # 1. 检测跳过的内容
    skipped = find_skipped_sections(alignment)
    for section in skipped:
        suggestions.append({
            'type': 'skipped_content',
            'section_id': section.id,
            'suggestion': f'排练中跳过了"{section.title}"。考虑删除此节或简化内容。'
        })

    # 2. 检测超时段落
    for section in alignment.sections:
        ratio = section.actual_time / section.target_time
        if ratio > 1.3:
            suggestions.append({
                'type': 'over_time',
                'section_id': section.id,
                'ratio': ratio,
                'suggestion': f'"{section.title}"超时 {int((ratio-1)*100)}%。建议精简内容。'
            })

    # 3. 检测填充词
    filler_words = ['嗯', '啊', '那个', '就是说', '然后']
    filler_count = {}
    for word in filler_words:
        count = recording_transcript.count(word)
        if count > 3:
            filler_count[word] = count

    if filler_count:
        suggestions.append({
            'type': 'filler_words',
            'counts': filler_count,
            'suggestion': '检测到较多填充词,建议用停顿代替。'
        })

    # 4. 整体时间评估
    time_diff = recording_duration - script.estimated_duration_seconds
    if abs(time_diff) > script.estimated_duration_seconds * 0.15:
        suggestions.append({
            'type': 'total_time_mismatch',
            'actual': recording_duration,
            'estimated': script.estimated_duration_seconds,
            'suggestion': f'实际时长与预估偏差 {abs(time_diff):.0f}秒。建议调整稿件长度。'
        })

    return RehearsalReport(
        alignment=alignment,
        suggestions=suggestions,
        overall_score=calculate_score(alignment, suggestions)
    )

7. 导出格式

7.1 演讲者手卡

---
手卡 #8 / 25
---
## 用户增长数据分析

[翻页] --> 显示折线图

"接下来我们看一下用户增长的关键数据。"

[点击] --> 高亮 7 月数据点

"从这张图可以看到,7月新功能上线后,
用户量从 50万 跃升到 110万。"

[停顿 2 秒]

"这意味着什么?意味着我们找到了产品的
核心价值点。"

---
时间预算: 45秒 | 字数: 85字
---

7.2 字幕文件

演讲稿 --> SRT 字幕格式

1
00:05:23,000 --> 00:05:28,000
接下来我们看一下用户增长的关键数据。

2
00:05:28,500 --> 00:05:36,000
从这张图可以看到,7月新功能上线后,
用户量从50万跃升到110万。

3
00:05:38,000 --> 00:05:44,000
这意味着什么?
意味着我们找到了产品的核心价值点。

7.3 TTS 语音合成

def generate_speech_audio(
    script: SpeechScript,
    voice_config: VoiceConfig
) -> AudioResult:
    """生成演讲语音"""

    segments = []
    for section in script.sections:
        for beat in section.beats:
            # 生成语音
            audio = tts_engine.synthesize(
                text=beat.text,
                voice=voice_config.voice_id,
                speed=voice_config.speed,
                pitch=voice_config.pitch
            )

            # 在句末添加停顿
            if beat.notes and '停顿' in beat.notes:
                pause_duration = extract_pause_duration(beat.notes)
                audio = append_silence(audio, pause_duration)

            segments.append({
                'beat_id': beat.beat_id,
                'audio': audio,
                'duration': audio.duration,
                'slide_cue': beat.cue
            })

    # 拼接所有片段
    full_audio = concatenate_audio(segments)

    # 生成同步时间轴
    timeline = build_sync_timeline(segments)

    return AudioResult(audio=full_audio, timeline=timeline)

8. 技术栈

组件 推荐方案 备选方案
演讲稿 LLM Claude Sonnet / GPT-4o DeepSeek
语速估算 自研规则引擎 --
语音转文字 Whisper Large V3 Azure STT
文字转语音 OpenAI TTS Azure TTS / 火山引擎
文本对齐 DTW (Dynamic Time Warping) Smith-Waterman
排练分析 自研 + LLM 总结 --
演讲者视图 Web (React/Vue) Electron
字幕生成 whisper-timestamped --

9. 完整工作流

1. 用户输入主题和约束
       |
2. AI 生成共享大纲(Section + Beat)
       |
3. 双轨并行生成:
   |                          |
   v                          v
   演讲稿轨道:                幻灯片轨道:
   - Beat 展开为口语文本      - Beat 映射为页面
   - 衔接过渡句               - 选择布局模板
   - 时间校准                 - 生成图表/图片
   - Cue 标注                 - 编排动画
       |                          |
4. 同步校验:
   - 每个 beat 在两侧都有对应
   - 时间预算一致
   - Cue 点与动画点对齐
       |
5. 排练与优化:
   - 录音排练
   - 对比分析
   - 自动建议
   - 稿件/幻灯片双轨更新
       |
6. 最终导出:
   - PPTX + 演讲者备注
   - 手卡 PDF
   - SRT 字幕
   - TTS 语音(可选)

Maurice | maurice_wen@proton.me