# 台词时长预估技术方案 > **文档版本**:v1.0 > **最后更新**:2026-02-06 > **用途**:说明台词时长预估的计算方法和实现逻辑 --- ## 目录 1. [业务背景](#业务背景) 2. [数据库设计](#数据库设计) 3. [计算公式](#计算公式) 4. [后端实现](#后端实现) 5. [分镜时长计算](#分镜时长计算) 6. [测试用例](#测试用例) --- ## 业务背景 ### 为什么需要预估台词时长? 1. **分镜时长规划**:用户需要知道每个分镜大概多长时间 2. **时间轴编辑**:在视频编辑器中需要准确的时长信息 3. **视频生成**:AI生成视频时需要知道生成多长 4. **音频同步**:TTS音频需要与画面精确对齐 ### 时长的两个阶段 | 阶段 | 字段 | 说明 | 用途 | |------|------|------|------| | AI解析阶段 | `estimated_duration` | 根据字数和语速预估 | 分镜规划、时间轴编辑 | | TTS生成阶段 | `actual_duration` | TTS生成后的真实音频时长 | 最终视频合成、精确同步 | --- ## 数据库设计 ### 表结构调整 ```sql -- 修改 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 | 极短停顿 | --- ## 后端实现 ### 台词时长计算函数 ```python # 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 层集成 ```python # 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} ) ``` --- ## 分镜时长计算 ### 分镜时长组成 ``` 分镜时长 = 台词总时长 + 台词间隔 + 画面时长 + 缓冲时间 ``` ### 计算逻辑 ```python # 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:普通对白 ```python 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 ```python 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:旁白 ```python 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:分镜时长(有台词) ```python 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:分镜时长(无台词) ```python 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生成后的时长更新 ### 更新实际时长 ```python # 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