refactor(video): 重构视频裁切功能实现

- 将 crop_size 字段替换为 crop_scale 浮点数字段,支持缩放倍率控制
- 将 face_pos 字段重命名为 crop_pos,统一裁切位置控制
- 移除 zoom_cut 和 crop_size 字段,简化裁切参数
- 新增 _build_crop_filter 静态方法,统一构建裁切滤镜逻辑
- 优化裁切算法,支持按目标比例和倍率进行精确裁切
- 统一处理图像和视频的裁切逻辑,消除代码重复
- 添加 cropScale 参数的安全解析,防止非法数值导致错误
- 改进裁切位置解析,支持浮点数坐标并添加异常处理
This commit is contained in:
2026-02-27 13:37:42 +08:00
parent 9dd5b6237d
commit 34e7d84d52
2 changed files with 57 additions and 43 deletions

View File

@@ -216,14 +216,13 @@ class RenderSpec:
用于 RENDER_SEGMENT_VIDEO 任务,定义视频渲染参数。 用于 RENDER_SEGMENT_VIDEO 任务,定义视频渲染参数。
""" """
crop_enable: bool = False crop_enable: bool = False
crop_size: Optional[str] = None crop_scale: float = 1.0
speed: str = "1.0" speed: str = "1.0"
lut_url: Optional[str] = None lut_url: Optional[str] = None
overlay_url: Optional[str] = None overlay_url: Optional[str] = None
effects: Optional[str] = None effects: Optional[str] = None
zoom_cut: bool = False
video_crop: Optional[str] = None video_crop: Optional[str] = None
face_pos: Optional[str] = None crop_pos: Optional[str] = None
transitions: Optional[str] = None transitions: Optional[str] = None
# 转场配置(PRD v2 新增) # 转场配置(PRD v2 新增)
transition_in: Optional[TransitionConfig] = None # 入场转场 transition_in: Optional[TransitionConfig] = None # 入场转场
@@ -234,16 +233,24 @@ class RenderSpec:
"""从字典创建 RenderSpec""" """从字典创建 RenderSpec"""
if not data: if not data:
return cls() return cls()
# 安全解析 cropScale:接受浮点数或字符串浮点数,非法值回退到 1.0
try:
crop_scale = float(data.get('cropScale', 1.0))
if crop_scale <= 0 or not isfinite(crop_scale):
crop_scale = 1.0
except (ValueError, TypeError):
crop_scale = 1.0
return cls( return cls(
crop_enable=data.get('cropEnable', False), crop_enable=data.get('cropEnable', False),
crop_size=data.get('cropSize'), crop_scale=crop_scale,
speed=str(data.get('speed', '1.0')), speed=str(data.get('speed', '1.0')),
lut_url=data.get('lutUrl'), lut_url=data.get('lutUrl'),
overlay_url=data.get('overlayUrl'), overlay_url=data.get('overlayUrl'),
effects=data.get('effects'), effects=data.get('effects'),
zoom_cut=data.get('zoomCut', False),
video_crop=data.get('videoCrop'), video_crop=data.get('videoCrop'),
face_pos=data.get('facePos'), crop_pos=data.get('cropPos'),
transitions=data.get('transitions'), transitions=data.get('transitions'),
transition_in=TransitionConfig.from_dict(data.get('transitionIn')), transition_in=TransitionConfig.from_dict(data.get('transitionIn')),
transition_out=TransitionConfig.from_dict(data.get('transitionOut')) transition_out=TransitionConfig.from_dict(data.get('transitionOut'))

View File

@@ -325,6 +325,43 @@ class RenderSegmentTsHandler(BaseHandler):
finally: finally:
self.cleanup_work_dir(work_dir) self.cleanup_work_dir(work_dir)
@staticmethod
def _build_crop_filter(
render_spec: 'RenderSpec',
width: int,
height: int,
task_id: str = ''
) -> Optional[str]:
"""
构建裁切滤镜
crop_enable 时:以目标比例为基准,按 crop_scale 倍率裁切,crop_pos 控制位置(默认居中)。
Returns:
crop 滤镜字符串,无需裁切时返回 None
"""
if render_spec.crop_enable:
scale = render_spec.crop_scale
target_ratio = width / height
# 解析裁切位置,默认居中
fx, fy = 0.5, 0.5
if render_spec.crop_pos:
try:
fx, fy = map(float, render_spec.crop_pos.split(','))
except ValueError:
logger.warning(f"[task:{task_id}] Invalid crop position: {render_spec.crop_pos}, using center")
fx, fy = 0.5, 0.5
# 基准:源中最大的目标比例矩形,再除以倍率
return (
f"crop='min(iw,ih*{target_ratio})/{scale}':'min(ih,iw/{target_ratio})/{scale}':"
f"'(iw-min(iw,ih*{target_ratio})/{scale})*{fx}':"
f"'(ih-min(ih,iw/{target_ratio})/{scale})*{fy}'"
)
return None
def _convert_image_to_video( def _convert_image_to_video(
self, self,
image_file: str, image_file: str,
@@ -373,23 +410,10 @@ class RenderSegmentTsHandler(BaseHandler):
# 构建滤镜:缩放填充到目标尺寸 # 构建滤镜:缩放填充到目标尺寸
filters = [] filters = []
# 裁切处理(与视频相同逻辑) # 裁切处理
if render_spec.crop_enable and render_spec.face_pos: crop_filter = self._build_crop_filter(render_spec, width, height, task_id)
try: if crop_filter:
fx, fy = map(float, render_spec.face_pos.split(',')) filters.append(crop_filter)
target_ratio = width / height
filters.append(
f"crop='min(iw,ih*{target_ratio})':'min(ih,iw/{target_ratio})':"
f"'(iw-min(iw,ih*{target_ratio}))*{fx}':"
f"'(ih-min(ih,iw/{target_ratio}))*{fy}'"
)
except (ValueError, ZeroDivisionError):
logger.warning(f"[task:{task_id}] Invalid face position: {render_spec.face_pos}")
elif render_spec.zoom_cut:
target_ratio = width / height
filters.append(
f"crop='min(iw,ih*{target_ratio})':'min(ih,iw/{target_ratio})'"
)
# 缩放填充 # 缩放填充
filters.append( filters.append(
@@ -711,26 +735,9 @@ class RenderSegmentTsHandler(BaseHandler):
filters.append(f"lut3d='{lut_path}'") filters.append(f"lut3d='{lut_path}'")
# 3. 裁切处理 # 3. 裁切处理
if render_spec.crop_enable and render_spec.face_pos: crop_filter = self._build_crop_filter(render_spec, width, height)
# 根据人脸位置进行智能裁切 if crop_filter:
try: filters.append(crop_filter)
fx, fy = map(float, render_spec.face_pos.split(','))
# 计算裁切区域(保持输出比例)
target_ratio = width / height
# 假设裁切到目标比例
filters.append(
f"crop='min(iw,ih*{target_ratio})':'min(ih,iw/{target_ratio})':"
f"'(iw-min(iw,ih*{target_ratio}))*{fx}':"
f"'(ih-min(ih,iw/{target_ratio}))*{fy}'"
)
except (ValueError, ZeroDivisionError):
logger.warning(f"Invalid face position: {render_spec.face_pos}")
elif render_spec.zoom_cut:
# 中心缩放裁切
target_ratio = width / height
filters.append(
f"crop='min(iw,ih*{target_ratio})':'min(ih,iw/{target_ratio})'"
)
# 4. 缩放和填充 # 4. 缩放和填充
scale_filter = ( scale_filter = (