台词时长预估技术方案
文档版本:v1.0
最后更新:2026-02-06
用途:说明台词时长预估的计算方法和实现逻辑
目录
- 业务背景
- 数据库设计
- 计算公式
- 后端实现
- 分镜时长计算
- 测试用例
业务背景
为什么需要预估台词时长?
- 分镜时长规划:用户需要知道每个分镜大概多长时间
- 时间轴编辑:在视频编辑器中需要准确的时长信息
- 视频生成:AI生成视频时需要知道生成多长
- 音频同步: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生成后填充) |
数据流程:
- AI解析剧本 → 返回台词内容和情绪
- 后端计算
estimated_duration → 存储到数据库
- TTS生成音频 → 获取音频时长 → 更新
actual_duration
- 视频合成 → 使用
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}
)
总结
核心要点
- AI不负责计算时长:AI只返回台词内容和情绪,时长由后端计算
- 两个时长字段:
estimated_duration(预估)和 actual_duration(实际)
- 标准公式:基于播音主持行业标准,考虑情绪和对白类型
- 分镜时长:综合考虑台词、画面、运镜、缓冲等因素
- 动态更新:TTS生成后更新实际时长,用于最终视频合成
实施步骤
- ✅ 修改数据库表结构(添加
estimated_duration 字段)
- ✅ 实现台词时长计算函数
- ✅ 实现分镜时长计算函数
- ✅ 集成到 Service 层
- ✅ 编写单元测试
- ⚪ 数据库迁移脚本
- ⚪ API文档更新
文档版本:v1.0
最后更新:2026-02-06