You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

16 KiB

台词时长预估技术方案

文档版本:v1.0
最后更新:2026-02-06
用途:说明台词时长预估的计算方法和实现逻辑


目录

  1. 业务背景
  2. 数据库设计
  3. 计算公式
  4. 后端实现
  5. 分镜时长计算
  6. 测试用例

业务背景

为什么需要预估台词时长?

  1. 分镜时长规划:用户需要知道每个分镜大概多长时间
  2. 时间轴编辑:在视频编辑器中需要准确的时长信息
  3. 视频生成:AI生成视频时需要知道生成多长
  4. 音频同步:TTS音频需要与画面精确对齐

时长的两个阶段

阶段 字段 说明 用途
AI解析阶段 estimated_duration 根据字数和语速预估 分镜规划、时间轴编辑
TTS生成阶段 actual_duration TTS生成后的真实音频时长 最终视频合成、精确同步

数据库设计

表结构调整

-- 修改 storyboard_dialogues 表
ALTER TABLE storyboard_dialogues 
ADD COLUMN estimated_duration NUMERIC(10, 3);

ALTER TABLE storyboard_dialogues 
RENAME COLUMN duration TO actual_duration;

-- 添加注释
COMMENT ON COLUMN storyboard_dialogues.estimated_duration IS '预估时长(秒,根据字数和语速计算)';
COMMENT ON COLUMN storyboard_dialogues.actual_duration IS '实际时长(秒,TTS生成后的真实音频时长)';

字段说明

字段 类型 必填 说明
estimated_duration NUMERIC(10,3) 预估时长(AI解析时计算)
actual_duration NUMERIC(10,3) 实际时长(TTS生成后填充)

数据流程

  1. AI解析剧本 → 返回台词内容和情绪
  2. 后端计算 estimated_duration → 存储到数据库
  3. TTS生成音频 → 获取音频时长 → 更新 actual_duration
  4. 视频合成 → 使用 actual_duration 进行精确同步

计算公式

中文语速标准

基于播音主持行业标准:

语速类型 字/秒 字/分钟 适用场景
正常语速 4.5 270 日常对话、新闻播报
快速语速 5.5 330 激动、兴奋、紧张
慢速语速 3.5 210 悲伤、疲惫、旁白

基础公式

台词时长 = (字数 / 调整后语速) + 标点停顿时间

调整后语速 = 基础语速 × 情绪系数 × 对白类型系数

情绪系数

情绪 系数 说明
高兴 1.1 语速加快10%
兴奋 1.2 语速加快20%
愤怒 1.15 语速加快15%
悲伤 0.85 语速减慢15%
平静 1.0 正常语速
紧张 1.1 语速加快10%
疲惫 0.9 语速减慢10%
温柔 0.95 语速稍慢5%
严肃 1.0 正常语速

对白类型系数

对白类型 系数 说明
普通对白 (1) 1.0 正常语速
内心OS (2) 0.9 稍慢,更有思考感
旁白 (3) 0.85 更慢、更清晰

标点停顿时间

标点符号 停顿时间(秒) 说明
逗号(,) 0.25 短暂停顿
句号(。) 0.5 正常停顿
问号(?) 0.5 正常停顿
感叹号(!) 0.5 正常停顿
省略号(…) 0.6 较长停顿
顿号(、) 0.2 极短停顿

后端实现

台词时长计算函数

# app/utils/dialogue_duration.py
from typing import Optional

def calculate_dialogue_duration(
    content: str,
    emotion: Optional[str] = None,
    dialogue_type: int = 1
) -> float:
    """计算台词预估时长(秒)
    
    Args:
        content: 台词内容
        emotion: 情绪标记(如"高兴"、"悲伤")
        dialogue_type: 对白类型(1=普通, 2=内心OS, 3=旁白)
    
    Returns:
        预估时长(秒)
    
    Examples:
        >>> calculate_dialogue_duration("你好,最近怎么样?", emotion="高兴")
        2.8
        
        >>> calculate_dialogue_duration("我该怎么办呢……", emotion="紧张", dialogue_type=2)
        2.9
    """
    # 1. 计算中文字符数
    chinese_chars = len([c for c in content if '\u4e00' <= c <= '\u9fff'])
    
    # 如果没有中文字符,返回最小时长
    if chinese_chars == 0:
        return 0.5
    
    # 2. 基础语速(字/秒)
    base_speed = 4.5  # 正常语速
    
    # 3. 情绪调整系数
    emotion_factor = {
        '高兴': 1.1, '兴奋': 1.2, '愤怒': 1.15,
        '悲伤': 0.85, '平静': 1.0, '紧张': 1.1,
        '疲惫': 0.9, '温柔': 0.95, '严肃': 1.0
    }.get(emotion, 1.0)
    
    # 4. 对白类型调整系数
    type_factor = {
        1: 1.0,   # 普通对白:正常语速
        2: 0.9,   # 内心OS:稍慢
        3: 0.85   # 旁白:更慢、更清晰
    }.get(dialogue_type, 1.0)
    
    # 5. 计算调整后的语速
    adjusted_speed = base_speed * emotion_factor * type_factor
    
    # 6. 计算基础时长
    base_duration = chinese_chars / adjusted_speed
    
    # 7. 计算标点停顿时间
    punctuation_pause = 0
    punctuation_pause += content.count(',') * 0.25  # 逗号
    punctuation_pause += content.count('。') * 0.5   # 句号
    punctuation_pause += content.count('?') * 0.5   # 问号
    punctuation_pause += content.count('!') * 0.5   # 感叹号
    punctuation_pause += content.count('…') * 0.6   # 省略号
    punctuation_pause += content.count('、') * 0.2   # 顿号
    
    # 8. 总时长
    total_duration = base_duration + punctuation_pause
    
    # 9. 最小时长限制(至少0.5秒)
    return max(total_duration, 0.5)

Service 层集成

# app/services/storyboard_service.py
from app.utils.dialogue_duration import calculate_dialogue_duration

async def create_storyboards_from_ai(
    self,
    screenplay_id: UUID,
    storyboards_data: List[Dict],
    tag_id_maps: Dict
) -> List[UUID]:
    """从 AI 解析结果创建分镜"""
    
    for storyboard_data in storyboards_data:
        # 1. 创建分镜记录
        storyboard = Storyboard(...)
        created_storyboard = await self.repository.create(storyboard)
        
        # 2. 创建对白并计算预估时长
        dialogues = []
        for dialogue_data in storyboard_data.get('dialogues', []):
            # 计算预估时长
            estimated_duration = calculate_dialogue_duration(
                content=dialogue_data['content'],
                emotion=dialogue_data.get('emotion'),
                dialogue_type=dialogue_data.get('dialogue_type', 1)
            )
            
            dialogue = StoryboardDialogue(
                storyboard_id=created_storyboard.storyboard_id,
                character_name=dialogue_data['character_name'],
                content=dialogue_data['content'],
                dialogue_type=dialogue_data['dialogue_type'],
                sequence_order=dialogue_data['sequence_order'],
                emotion=dialogue_data.get('emotion'),
                estimated_duration=estimated_duration,  # 预估时长
                actual_duration=None  # TTS生成后填充
            )
            
            created_dialogue = await self.repository.create_dialogue(dialogue)
            dialogues.append(created_dialogue)
        
        # 3. 计算分镜总时长
        storyboard_duration = calculate_storyboard_duration(
            storyboard_data,
            dialogues
        )
        
        # 4. 更新分镜时长
        await self.repository.update(
            created_storyboard.storyboard_id,
            {'estimated_duration': storyboard_duration}
        )

分镜时长计算

分镜时长组成

分镜时长 = 台词总时长 + 台词间隔 + 画面时长 + 缓冲时间

计算逻辑

# app/utils/storyboard_duration.py
from typing import List, Dict, Any

def calculate_storyboard_duration(
    storyboard_data: Dict[str, Any],
    dialogues: List[Any]
) -> float:
    """计算分镜预估时长
    
    Args:
        storyboard_data: 分镜数据(包含shot_size, camera_movement等)
        dialogues: 对白对象数组(已包含estimated_duration)
    
    Returns:
        预估时长(秒)
    """
    # 1. 计算所有台词的总时长
    total_dialogue_duration = sum(
        d.estimated_duration for d in dialogues if d.estimated_duration
    )
    
    # 2. 计算台词间隔时间
    dialogue_gap = 0
    if len(dialogues) > 1:
        dialogue_gap = (len(dialogues) - 1) * 0.5  # 每个间隔0.5秒
    
    # 3. 基础画面时长(根据景别)
    shot_size_duration = {
        1: 5.0,  # 大远景:5秒(建立场景)
        2: 4.0,  # 远景:4秒
        3: 3.0,  # 全景:3秒
        4: 2.5,  # 中景:2.5秒
        5: 2.0,  # 中近景:2秒
        6: 1.5,  # 特写:1.5秒
        7: 1.0,  # 大特写:1秒
        8: 2.5   # 过肩镜头:2.5秒
    }.get(storyboard_data.get('shot_size'), 3.0)
    
    # 4. 运镜时间调整系数
    camera_movement_factor = {
        1: 1.0,   # 固定:无额外时间
        2: 1.2,   # 摇镜:增加20%
        3: 1.2,   # 俯仰:增加20%
        4: 1.3,   # 推拉:增加30%
        5: 1.1,   # 变焦:增加10%
        6: 1.4,   # 跟踪:增加40%
        7: 1.5,   # 环绕:增加50%
        8: 1.3,   # 升降:增加30%
        9: 1.1    # 手持:增加10%
    }.get(storyboard_data.get('camera_movement'), 1.0)
    
    adjusted_shot_duration = shot_size_duration * camera_movement_factor
    
    # 5. 前后缓冲时间
    buffer_time = 1.5  # 前0.8秒 + 后0.7秒
    
    # 6. 计算总时长
    if total_dialogue_duration > 0:
        # 有台词:台词时长 + 间隔 + 缓冲
        total_duration = total_dialogue_duration + dialogue_gap + buffer_time
    else:
        # 无台词:画面时长 + 缓冲
        total_duration = adjusted_shot_duration + buffer_time
    
    # 7. 最小时长限制(至少2秒)
    return max(total_duration, 2.0)

景别与画面时长对照表

景别 代码 基础时长 说明
大远景 1 5.0秒 建立场景,展示环境
远景 2 4.0秒 展示环境和人物关系
全景 3 3.0秒 展示人物全身
中景 4 2.5秒 腰部以上
中近景 5 2.0秒 胸部/肩部以上
特写 6 1.5秒 面部特写
大特写 7 1.0秒 局部细节
过肩镜头 8 2.5秒 对话场景

运镜时间调整系数

运镜方式 代码 系数 说明
固定 1 1.0 无额外时间
摇镜 2 1.2 增加20%
俯仰 3 1.2 增加20%
推拉 4 1.3 增加30%
变焦 5 1.1 增加10%
跟踪 6 1.4 增加40%
环绕 7 1.5 增加50%
升降 8 1.3 增加30%
手持 9 1.1 增加10%

测试用例

测试用例1:普通对白

def test_normal_dialogue():
    content = "你好,最近怎么样?"
    duration = calculate_dialogue_duration(content, emotion="高兴")
    
    # 预期计算:
    # 字数:9个中文字符
    # 基础语速:4.5字/秒
    # 情绪系数:1.1(高兴)
    # 对白类型系数:1.0(普通)
    # 调整后语速:4.5 * 1.1 * 1.0 = 4.95字/秒
    # 基础时长:9 / 4.95 = 1.82秒
    # 标点停顿:0.25(逗号) + 0.5(问号) = 0.75秒
    # 总时长:1.82 + 0.75 = 2.57秒
    
    assert 2.5 <= duration <= 2.7

测试用例2:内心OS

def test_inner_monologue():
    content = "我该怎么办呢……"
    duration = calculate_dialogue_duration(
        content, 
        emotion="紧张", 
        dialogue_type=2
    )
    
    # 预期计算:
    # 字数:7个中文字符
    # 基础语速:4.5字/秒
    # 情绪系数:1.1(紧张)
    # 对白类型系数:0.9(内心OS)
    # 调整后语速:4.5 * 1.1 * 0.9 = 4.455字/秒
    # 基础时长:7 / 4.455 = 1.57秒
    # 标点停顿:0.6(省略号) = 0.6秒
    # 总时长:1.57 + 0.6 = 2.17秒
    
    assert 2.1 <= duration <= 2.3

测试用例3:旁白

def test_narration():
    content = "在一个阳光明媚的早晨,张三来到了咖啡厅。"
    duration = calculate_dialogue_duration(content, dialogue_type=3)
    
    # 预期计算:
    # 字数:20个中文字符
    # 基础语速:4.5字/秒
    # 情绪系数:1.0(无情绪)
    # 对白类型系数:0.85(旁白)
    # 调整后语速:4.5 * 1.0 * 0.85 = 3.825字/秒
    # 基础时长:20 / 3.825 = 5.23秒
    # 标点停顿:0.25(逗号) + 0.5(句号) = 0.75秒
    # 总时长:5.23 + 0.75 = 5.98秒
    
    assert 5.8 <= duration <= 6.2

测试用例4:分镜时长(有台词)

def test_storyboard_with_dialogues():
    storyboard_data = {
        'shot_size': 4,  # 中景
        'camera_movement': 1  # 固定
    }
    
    # 模拟对白对象
    class MockDialogue:
        def __init__(self, duration):
            self.estimated_duration = duration
    
    dialogues = [
        MockDialogue(2.5),  # 第一条台词
        MockDialogue(2.0)   # 第二条台词
    ]
    
    duration = calculate_storyboard_duration(storyboard_data, dialogues)
    
    # 预期计算:
    # 台词总时长:2.5 + 2.0 = 4.5秒
    # 台词间隔:1 * 0.5 = 0.5秒
    # 缓冲时间:1.5秒
    # 总时长:4.5 + 0.5 + 1.5 = 6.5秒
    
    assert 6.4 <= duration <= 6.6

测试用例5:分镜时长(无台词)

def test_storyboard_without_dialogues():
    storyboard_data = {
        'shot_size': 1,  # 大远景
        'camera_movement': 7  # 环绕
    }
    
    dialogues = []
    
    duration = calculate_storyboard_duration(storyboard_data, dialogues)
    
    # 预期计算:
    # 基础画面时长:5.0秒(大远景)
    # 运镜系数:1.5(环绕)
    # 调整后画面时长:5.0 * 1.5 = 7.5秒
    # 缓冲时间:1.5秒
    # 总时长:7.5 + 1.5 = 9.0秒
    
    assert 8.9 <= duration <= 9.1

TTS生成后的时长更新

更新实际时长

# app/services/storyboard_resource_service.py
async def update_dialogue_actual_duration(
    self,
    dialogue_id: UUID,
    audio_duration: float
) -> None:
    """更新对白的实际时长(TTS生成后调用)
    
    Args:
        dialogue_id: 对白ID
        audio_duration: TTS生成的音频实际时长(秒)
    """
    await self.repository.update_dialogue(
        dialogue_id,
        {'actual_duration': audio_duration}
    )
    
    # 重新计算分镜的实际时长
    dialogue = await self.repository.get_dialogue_by_id(dialogue_id)
    await self._recalculate_storyboard_actual_duration(
        dialogue.storyboard_id
    )

async def _recalculate_storyboard_actual_duration(
    self,
    storyboard_id: UUID
) -> None:
    """重新计算分镜的实际时长"""
    # 获取所有对白
    dialogues = await self.repository.get_dialogues_by_storyboard(
        storyboard_id
    )
    
    # 计算实际时长总和
    total_actual_duration = sum(
        d.actual_duration for d in dialogues 
        if d.actual_duration is not None
    )
    
    # 如果所有对白都有实际时长,更新分镜的实际时长
    if all(d.actual_duration is not None for d in dialogues):
        # 加上间隔和缓冲
        dialogue_gap = (len(dialogues) - 1) * 0.5 if len(dialogues) > 1 else 0
        buffer_time = 1.5
        
        actual_duration = total_actual_duration + dialogue_gap + buffer_time
        
        await self.repository.update_storyboard(
            storyboard_id,
            {'actual_duration': actual_duration}
        )

总结

核心要点

  1. AI不负责计算时长:AI只返回台词内容和情绪,时长由后端计算
  2. 两个时长字段estimated_duration(预估)和 actual_duration(实际)
  3. 标准公式:基于播音主持行业标准,考虑情绪和对白类型
  4. 分镜时长:综合考虑台词、画面、运镜、缓冲等因素
  5. 动态更新:TTS生成后更新实际时长,用于最终视频合成

实施步骤

  1. 修改数据库表结构(添加 estimated_duration 字段)
  2. 实现台词时长计算函数
  3. 实现分镜时长计算函数
  4. 集成到 Service 层
  5. 编写单元测试
  6. 数据库迁移脚本
  7. API文档更新

文档版本:v1.0
最后更新:2026-02-06