diff --git a/domain/task.py b/domain/task.py index fc06f40..ad04d66 100644 --- a/domain/task.py +++ b/domain/task.py @@ -5,12 +5,13 @@ 定义任务类型、任务实体、渲染规格、输出规格等数据结构。 """ -from enum import Enum -from dataclasses import dataclass, field -from typing import Dict, Any, Optional, List -from datetime import datetime -from urllib.parse import urlparse, unquote import os +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from math import isfinite +from typing import Dict, Any, Optional, List +from urllib.parse import urlparse, unquote # 支持的图片扩展名 @@ -95,7 +96,9 @@ class Effect: 特效配置 格式:type:params - 例如:cameraShot:3,1 表示在第3秒定格1秒 + 例如: + - cameraShot:3,1 表示在第3秒定格1秒 + - zoom:1.5,1.2,2 表示从第1.5秒开始放大 1.2 倍并持续 2 秒 """ effect_type: str # 效果类型 params: str = "" # 参数字符串 @@ -122,7 +125,7 @@ class Effect: 解析效果字符串 格式:effect1|effect2|effect3 - 例如:cameraShot:3,1|blur:5 + 例如:cameraShot:3,1|zoom:1.5,1.2,2 """ if not effects_str: return [] @@ -152,6 +155,40 @@ class Effect: except ValueError: return (3, 1) + def get_zoom_params(self) -> tuple: + """ + 获取 zoom 效果参数 + + Returns: + (start_sec, scale_factor, duration_sec): 起始时间、放大倍数、持续时长(秒) + """ + if self.effect_type != 'zoom': + return (0.0, 1.2, 1.0) + + default_start_sec = 0.0 + default_scale_factor = 1.2 + default_duration_sec = 1.0 + + if not self.params: + return (default_start_sec, default_scale_factor, default_duration_sec) + + parts = [part.strip() for part in self.params.split(',')] + try: + start_sec = float(parts[0]) if len(parts) >= 1 and parts[0] else default_start_sec + scale_factor = float(parts[1]) if len(parts) >= 2 and parts[1] else default_scale_factor + duration_sec = float(parts[2]) if len(parts) >= 3 and parts[2] else default_duration_sec + except ValueError: + return (default_start_sec, default_scale_factor, default_duration_sec) + + if not isfinite(start_sec) or start_sec < 0: + start_sec = default_start_sec + if not isfinite(scale_factor) or scale_factor <= 1.0: + scale_factor = default_scale_factor + if not isfinite(duration_sec) or duration_sec <= 0: + duration_sec = default_duration_sec + + return (start_sec, scale_factor, duration_sec) + @dataclass class RenderSpec: diff --git a/handlers/render_video.py b/handlers/render_video.py index d3d7749..a188a0a 100644 --- a/handlers/render_video.py +++ b/handlers/render_video.py @@ -491,7 +491,10 @@ class RenderSegmentVideoHandler(BaseHandler): # 解析 effects effects = render_spec.get_effects() - has_camera_shot = any(e.effect_type == 'cameraShot' for e in effects) + has_complex_effect = any( + effect.effect_type in {'cameraShot', 'zoom'} + for effect in effects + ) # 硬件加速时需要先 hwdownload(将 GPU 表面下载到系统内存) hwaccel_prefix = self.get_hwaccel_filter_prefix() @@ -541,9 +544,8 @@ class RenderSegmentVideoHandler(BaseHandler): ) filters.append(scale_filter) - # 5. 特效处理(cameraShot 需要特殊处理) - if has_camera_shot: - # cameraShot 需要使用 filter_complex 格式 + # 5. 特效处理(cameraShot / zoom 需要使用 filter_complex) + if has_complex_effect: return self._build_filter_complex_with_effects( base_filters=filters, effects=effects, @@ -624,7 +626,7 @@ class RenderSegmentVideoHandler(BaseHandler): """ 构建包含特效的 filter_complex 滤镜图 - cameraShot 效果需要使用 split/freezeframes/concat 滤镜组合。 + cameraShot / zoom 效果都在此处统一处理并按 effects 顺序叠加。 Args: base_filters: 基础滤镜列表 @@ -695,6 +697,31 @@ class RenderSegmentVideoHandler(BaseHandler): f"{frozen_out}{rest_out}concat=n=2:v=1:a=0{effect_output}" ) + current_output = effect_output + effect_idx += 1 + elif effect.effect_type == 'zoom': + start_sec, scale_factor, duration_sec = effect.get_zoom_params() + if start_sec < 0 or scale_factor <= 1.0 or duration_sec <= 0: + continue + + zoom_end_sec = start_sec + duration_sec + base_out = f'[eff{effect_idx}_base]' + zoom_source_out = f'[eff{effect_idx}_zoom_src]' + zoom_scaled_out = f'[eff{effect_idx}_zoom_scaled]' + effect_output = f'[v_eff{effect_idx}]' + zoom_enable = f"'between(t,{start_sec},{zoom_end_sec})'" + + filter_parts.append( + f"{current_output}split=2{base_out}{zoom_source_out}" + ) + filter_parts.append( + f"{zoom_source_out}scale=iw*{scale_factor}:ih*{scale_factor}," + f"crop={width}:{height}:(in_w-{width})/2:(in_h-{height})/2{zoom_scaled_out}" + ) + filter_parts.append( + f"{base_out}{zoom_scaled_out}overlay=0:0:enable={zoom_enable}{effect_output}" + ) + current_output = effect_output effect_idx += 1