You've already forked FrameTour-RenderWorker
feat(video): 添加视频转场功能支持
- 在 TASK_TYPES 中新增 COMPOSE_TRANSITION 类型 - 定义 TRANSITION_TYPES 常量支持多种转场效果 - 在 TaskType 枚举中添加 COMPOSE_TRANSITION - 创建 TransitionConfig 数据类处理转场配置 - 为 RenderSpec 添加 transition_in 和 transition_out 属性 - 在 Task 类中添加转场相关的方法 - 新增 ComposeTransitionHandler 处理转场合成任务 - 修改 PackageSegmentTsHandler 支持转场分片封装 - 修改 RenderSegmentVideoHandler 支持 overlap 区域生成 - 在 TaskExecutor 中注册转场处理器
This commit is contained in:
116
domain/task.py
116
domain/task.py
@@ -14,11 +14,27 @@ from datetime import datetime
|
||||
class TaskType(Enum):
|
||||
"""任务类型枚举"""
|
||||
RENDER_SEGMENT_VIDEO = "RENDER_SEGMENT_VIDEO" # 渲染视频片段
|
||||
COMPOSE_TRANSITION = "COMPOSE_TRANSITION" # 合成转场效果
|
||||
PREPARE_JOB_AUDIO = "PREPARE_JOB_AUDIO" # 生成全局音频
|
||||
PACKAGE_SEGMENT_TS = "PACKAGE_SEGMENT_TS" # 封装 TS 分片
|
||||
FINALIZE_MP4 = "FINALIZE_MP4" # 产出最终 MP4
|
||||
|
||||
|
||||
# 支持的转场类型(对应 FFmpeg xfade 参数)
|
||||
TRANSITION_TYPES = {
|
||||
'fade': 'fade', # 淡入淡出(默认)
|
||||
'dissolve': 'dissolve', # 溶解过渡
|
||||
'wipeleft': 'wipeleft', # 向左擦除
|
||||
'wiperight': 'wiperight', # 向右擦除
|
||||
'wipeup': 'wipeup', # 向上擦除
|
||||
'wipedown': 'wipedown', # 向下擦除
|
||||
'slideleft': 'slideleft', # 向左滑动
|
||||
'slideright': 'slideright', # 向右滑动
|
||||
'slideup': 'slideup', # 向上滑动
|
||||
'slidedown': 'slidedown', # 向下滑动
|
||||
}
|
||||
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""任务状态枚举"""
|
||||
PENDING = "PENDING"
|
||||
@@ -27,6 +43,39 @@ class TaskStatus(Enum):
|
||||
FAILED = "FAILED"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransitionConfig:
|
||||
"""
|
||||
转场配置
|
||||
|
||||
用于 RENDER_SEGMENT_VIDEO 任务的入场/出场转场配置。
|
||||
"""
|
||||
type: str = "fade" # 转场类型
|
||||
duration_ms: int = 500 # 转场时长(毫秒)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Optional[Dict]) -> Optional['TransitionConfig']:
|
||||
"""从字典创建 TransitionConfig"""
|
||||
if not data:
|
||||
return None
|
||||
trans_type = data.get('type', 'fade')
|
||||
# 验证转场类型是否支持
|
||||
if trans_type not in TRANSITION_TYPES:
|
||||
trans_type = 'fade'
|
||||
return cls(
|
||||
type=trans_type,
|
||||
duration_ms=int(data.get('durationMs', 500))
|
||||
)
|
||||
|
||||
def get_overlap_ms(self) -> int:
|
||||
"""获取 overlap 时长(单边,为转场时长的一半)"""
|
||||
return self.duration_ms // 2
|
||||
|
||||
def get_ffmpeg_transition(self) -> str:
|
||||
"""获取 FFmpeg xfade 参数"""
|
||||
return TRANSITION_TYPES.get(self.type, 'fade')
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderSpec:
|
||||
"""
|
||||
@@ -44,6 +93,9 @@ class RenderSpec:
|
||||
video_crop: Optional[str] = None
|
||||
face_pos: Optional[str] = None
|
||||
transitions: Optional[str] = None
|
||||
# 转场配置(PRD v2 新增)
|
||||
transition_in: Optional[TransitionConfig] = None # 入场转场
|
||||
transition_out: Optional[TransitionConfig] = None # 出场转场
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Optional[Dict]) -> 'RenderSpec':
|
||||
@@ -60,9 +112,31 @@ class RenderSpec:
|
||||
zoom_cut=data.get('zoomCut', False),
|
||||
video_crop=data.get('videoCrop'),
|
||||
face_pos=data.get('facePos'),
|
||||
transitions=data.get('transitions')
|
||||
transitions=data.get('transitions'),
|
||||
transition_in=TransitionConfig.from_dict(data.get('transitionIn')),
|
||||
transition_out=TransitionConfig.from_dict(data.get('transitionOut'))
|
||||
)
|
||||
|
||||
def has_transition_in(self) -> bool:
|
||||
"""是否有入场转场"""
|
||||
return self.transition_in is not None and self.transition_in.duration_ms > 0
|
||||
|
||||
def has_transition_out(self) -> bool:
|
||||
"""是否有出场转场"""
|
||||
return self.transition_out is not None and self.transition_out.duration_ms > 0
|
||||
|
||||
def get_overlap_head_ms(self) -> int:
|
||||
"""获取头部 overlap 时长(毫秒)"""
|
||||
if self.has_transition_in():
|
||||
return self.transition_in.get_overlap_ms()
|
||||
return 0
|
||||
|
||||
def get_overlap_tail_ms(self) -> int:
|
||||
"""获取尾部 overlap 时长(毫秒)"""
|
||||
if self.has_transition_out():
|
||||
return self.transition_out.get_overlap_ms()
|
||||
return 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputSpec:
|
||||
@@ -247,3 +321,43 @@ class Task:
|
||||
def get_ts_list(self) -> List[str]:
|
||||
"""获取 TS 列表(用于 FINALIZE_MP4)"""
|
||||
return self.payload.get('tsList', [])
|
||||
|
||||
# ========== COMPOSE_TRANSITION 相关方法 ==========
|
||||
|
||||
def get_transition_id(self) -> Optional[str]:
|
||||
"""获取转场 ID(用于 COMPOSE_TRANSITION)"""
|
||||
return self.payload.get('transitionId')
|
||||
|
||||
def get_prev_segment(self) -> Optional[Dict]:
|
||||
"""获取前一个片段信息(用于 COMPOSE_TRANSITION)"""
|
||||
return self.payload.get('prevSegment')
|
||||
|
||||
def get_next_segment(self) -> Optional[Dict]:
|
||||
"""获取后一个片段信息(用于 COMPOSE_TRANSITION)"""
|
||||
return self.payload.get('nextSegment')
|
||||
|
||||
def get_transition_config(self) -> Optional[TransitionConfig]:
|
||||
"""获取转场配置(用于 COMPOSE_TRANSITION)"""
|
||||
return TransitionConfig.from_dict(self.payload.get('transition'))
|
||||
|
||||
# ========== PACKAGE_SEGMENT_TS 转场相关方法 ==========
|
||||
|
||||
def is_transition_segment(self) -> bool:
|
||||
"""是否为转场分片(用于 PACKAGE_SEGMENT_TS)"""
|
||||
return self.payload.get('isTransitionSegment', False)
|
||||
|
||||
def should_trim_head(self) -> bool:
|
||||
"""是否需要裁剪头部 overlap(用于 PACKAGE_SEGMENT_TS)"""
|
||||
return self.payload.get('trimHead', False)
|
||||
|
||||
def should_trim_tail(self) -> bool:
|
||||
"""是否需要裁剪尾部 overlap(用于 PACKAGE_SEGMENT_TS)"""
|
||||
return self.payload.get('trimTail', False)
|
||||
|
||||
def get_trim_head_ms(self) -> int:
|
||||
"""获取头部裁剪时长(毫秒)"""
|
||||
return int(self.payload.get('trimHeadMs', 0))
|
||||
|
||||
def get_trim_tail_ms(self) -> int:
|
||||
"""获取尾部裁剪时长(毫秒)"""
|
||||
return int(self.payload.get('trimTailMs', 0))
|
||||
|
||||
Reference in New Issue
Block a user