Files
CapCutAPI/pyJianYingDraft/video_segment.py

554 lines
23 KiB
Python

"""定义视频片段及其相关类
包含图像调节设置、动画效果、特效、转场等相关类
"""
import uuid
from copy import deepcopy
from typing import Optional, Literal, Union, overload
from typing import Dict, List, Tuple, Any
from pyJianYingDraft.metadata.capcut_effect_meta import CapCut_Video_character_effect_type, CapCut_Video_scene_effect_type
from pyJianYingDraft.metadata.capcut_mask_meta import CapCut_Mask_type
from settings import IS_CAPCUT_ENV
from .time_util import tim, Timerange
from .segment import Visual_segment, Clip_settings
from .local_materials import Video_material
from .animation import Segment_animations, Video_animation
from .metadata import Effect_meta, Effect_param_instance
from .metadata import Mask_meta, Mask_type, Filter_type, Transition_type, CapCut_Transition_type
from .metadata import Intro_type, Outro_type, Group_animation_type
from .metadata import CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type
from .metadata import Video_scene_effect_type, Video_character_effect_type
class Mask:
"""蒙版对象"""
mask_meta: Mask_meta
"""蒙版元数据"""
global_id: str
"""蒙版全局id, 由程序自动生成"""
center_x: float
"""蒙版中心x坐标, 以半素材宽为单位"""
center_y: float
"""蒙版中心y坐标, 以半素材高为单位"""
width: float
height: float
aspect_ratio: float
rotation: float
invert: bool
feather: float
"""羽化程度, 0-1"""
round_corner: float
"""矩形蒙版的圆角, 0-1"""
def __init__(self, mask_meta: Mask_meta,
cx: float, cy: float, w: float, h: float,
ratio: float, rot: float, inv: bool, feather: float, round_corner: float):
self.mask_meta = mask_meta
self.global_id = uuid.uuid4().hex
self.center_x, self.center_y = cx, cy
self.width, self.height = w, h
self.aspect_ratio = ratio
self.rotation = rot
self.invert = inv
self.feather = feather
self.round_corner = round_corner
def export_json(self) -> Dict[str, Any]:
return {
"config": {
"aspectRatio": self.aspect_ratio,
"centerX": self.center_x,
"centerY": self.center_y,
"feather": self.feather,
"height": self.height,
"invert": self.invert,
"rotation": self.rotation,
"roundCorner": self.round_corner,
"width": self.width
},
"category": "video",
"category_id": "",
"category_name": "",
"id": self.global_id,
"name": self.mask_meta.name,
"platform": "all",
"position_info": "",
"resource_type": self.mask_meta.resource_type,
"resource_id": self.mask_meta.resource_id,
"type": "mask"
# 不导出path字段
}
class Video_effect:
"""视频特效素材"""
name: str
"""特效名称"""
global_id: str
"""特效全局id, 由程序自动生成"""
effect_id: str
"""某种特效id, 由剪映本身提供"""
resource_id: str
"""资源id, 由剪映本身提供"""
effect_type: Literal["video_effect", "face_effect"]
apply_target_type: Literal[0, 2]
"""应用目标类型, 0: 片段, 2: 全局"""
adjust_params: List[Effect_param_instance]
def __init__(self, effect_meta: Union[Video_scene_effect_type, Video_character_effect_type],
params: Optional[List[Optional[float]]] = None, *,
apply_target_type: Literal[0, 2] = 0):
"""根据给定的特效元数据及参数列表构造一个视频特效对象, params的范围是0~100"""
self.name = effect_meta.value.name
self.global_id = uuid.uuid4().hex
self.effect_id = effect_meta.value.effect_id
self.resource_id = effect_meta.value.resource_id
self.adjust_params = []
if IS_CAPCUT_ENV:
if isinstance(effect_meta, CapCut_Video_scene_effect_type):
self.effect_type = "video_effect"
elif isinstance(effect_meta, CapCut_Video_character_effect_type):
self.effect_type = "face_effect"
else:
raise TypeError("Invalid effect meta type %s" % type(effect_meta))
else:
if isinstance(effect_meta, Video_scene_effect_type):
self.effect_type = "video_effect"
elif isinstance(effect_meta, Video_character_effect_type):
self.effect_type = "face_effect"
else:
raise TypeError("Invalid effect meta type %s" % type(effect_meta))
self.apply_target_type = apply_target_type
self.adjust_params = effect_meta.value.parse_params(params)
def export_json(self) -> Dict[str, Any]:
return {
"adjust_params": [param.export_json() for param in self.adjust_params],
"apply_target_type": self.apply_target_type,
"apply_time_range": None,
"category_id": "", # 一律设为空
"category_name": "", # 一律设为空
"common_keyframes": [],
"disable_effect_faces": [],
"effect_id": self.effect_id,
"formula_id": "",
"id": self.global_id,
"name": self.name,
"platform": "all",
"render_index": 11000,
"resource_id": self.resource_id,
"source_platform": 0,
"time_range": None,
"track_render_index": 0,
"type": self.effect_type,
"value": 1.0,
"version": ""
# 不导出path、request_id和algorithm_artifact_path字段
}
class Filter:
"""滤镜素材"""
global_id: str
"""滤镜全局id, 由程序自动生成"""
effect_meta: Effect_meta
"""滤镜的元数据"""
intensity: float
"""滤镜强度(滤镜的唯一参数)"""
apply_target_type: Literal[0, 2]
"""应用目标类型, 0: 片段, 2: 全局"""
def __init__(self, meta: Effect_meta, intensity: float, *,
apply_target_type: Literal[0, 2] = 0):
"""根据给定的滤镜元数据及强度构造滤镜素材对象"""
self.global_id = uuid.uuid4().hex
self.effect_meta = meta
self.intensity = intensity
self.apply_target_type = apply_target_type
def export_json(self) -> Dict[str, Any]:
return {
"adjust_params": [],
"algorithm_artifact_path": "",
"apply_target_type": self.apply_target_type,
"bloom_params": None,
"category_id": "", # 一律设为空
"category_name": "", # 一律设为空
"color_match_info": {
"source_feature_path": "",
"target_feature_path": "",
"target_image_path": ""
},
"effect_id": self.effect_meta.effect_id,
"enable_skin_tone_correction": False,
"exclusion_group": [],
"face_adjust_params": [],
"formula_id": "",
"id": self.global_id,
"intensity_key": "",
"multi_language_current": "",
"name": self.effect_meta.name,
"panel_id": "",
"platform": "all",
"resource_id": self.effect_meta.resource_id,
"source_platform": 1,
"sub_type": "none",
"time_range": None,
"type": "filter",
"value": self.intensity,
"version": ""
# 不导出path和request_id
}
class Transition:
"""转场对象"""
name: str
"""转场名称"""
global_id: str
"""转场全局id, 由程序自动生成"""
effect_id: str
"""转场效果id, 由剪映本身提供"""
resource_id: str
"""资源id, 由剪映本身提供"""
duration: int
"""转场持续时间, 单位为微秒"""
is_overlap: bool
"""是否与上一个片段重叠(?)"""
def __init__(self, effect_meta: Union[Transition_type, CapCut_Transition_type], duration: Optional[int] = None):
"""根据给定的转场元数据及持续时间构造一个转场对象"""
self.name = effect_meta.value.name
self.global_id = uuid.uuid4().hex
self.effect_id = effect_meta.value.effect_id
self.resource_id = effect_meta.value.resource_id
self.duration = duration if duration is not None else effect_meta.value.default_duration
self.is_overlap = effect_meta.value.is_overlap
def export_json(self) -> Dict[str, Any]:
return {
"category_id": "", # 一律设为空
"category_name": "", # 一律设为空
"duration": self.duration,
"effect_id": self.effect_id,
"id": self.global_id,
"is_overlap": self.is_overlap,
"name": self.name,
"platform": "all",
"resource_id": self.resource_id,
"type": "transition"
# 不导出path和request_id字段
}
class BackgroundFilling:
"""背景填充对象"""
global_id: str
"""背景填充全局id, 由程序自动生成"""
fill_type: Literal["canvas_blur", "canvas_color"]
"""背景填充类型"""
blur: float
"""模糊程度, 0-1"""
color: str
"""背景颜色, 格式为'#RRGGBBAA'"""
def __init__(self, fill_type: Literal["canvas_blur", "canvas_color"], blur: float, color: str):
self.global_id = uuid.uuid4().hex
self.fill_type = fill_type
self.blur = blur
self.color = color
def export_json(self) -> Dict[str, Any]:
return {
"id": self.global_id,
"type": self.fill_type,
"blur": self.blur,
"color": self.color,
"source_platform": 0,
}
class Video_segment(Visual_segment):
"""安放在轨道上的一个视频/图片片段"""
material_instance: Video_material
"""素材实例"""
material_size: Tuple[int, int]
"""素材尺寸"""
effects: List[Video_effect]
"""特效列表
在放入轨道时自动添加到素材列表中
"""
filters: List[Filter]
"""滤镜列表
在放入轨道时自动添加到素材列表中
"""
mask: Optional[Mask]
"""蒙版实例, 可能为空
在放入轨道时自动添加到素材列表中
"""
transition: Optional[Transition]
"""转场实例, 可能为空
在放入轨道时自动添加到素材列表中
"""
background_filling: Optional[BackgroundFilling]
"""背景填充实例, 可能为空
在放入轨道时自动添加到素材列表中
"""
visible: Optional[bool]
"""是否可见
默认为True
"""
# TODO: material参数接受path进行便捷构造
def __init__(self, material: Video_material, target_timerange: Timerange, *,
source_timerange: Optional[Timerange] = None, speed: Optional[float] = None, volume: float = 1.0,
clip_settings: Optional[Clip_settings] = None):
"""利用给定的视频/图片素材构建一个轨道片段, 并指定其时间信息及图像调节设置
Args:
material (`Video_material`): 素材实例
target_timerange (`Timerange`): 片段在轨道上的目标时间范围
source_timerange (`Timerange`, optional): 截取的素材片段的时间范围, 默认从开头根据`speed`截取与`target_timerange`等长的一部分
speed (`float`, optional): 播放速度, 默认为1.0. 此项与`source_timerange`同时指定时, 将覆盖`target_timerange`中的时长
volume (`float`, optional): 音量, 默认为1.0
clip_settings (`Clip_settings`, optional): 图像调节设置, 默认不作任何变换
Raises:
`ValueError`: 指定的或计算出的`source_timerange`超出了素材的时长范围
"""
# if source_timerange is not None and speed is not None:
# target_timerange = Timerange(target_timerange.start, round(source_timerange.duration / speed))
# elif source_timerange is not None and speed is None:
# speed = source_timerange.duration / target_timerange.duration
# else: # source_timerange is None
# speed = speed if speed is not None else 1.0
# source_timerange = Timerange(0, round(target_timerange.duration * speed))
# if source_timerange.end > material.duration:
# source_timerange = Timerange(source_timerange.start, material.duration - source_timerange.start)
# # 重新计算目标时间范围
# target_timerange = Timerange(target_timerange.start, round(source_timerange.duration / speed))
super().__init__(material.material_id, source_timerange, target_timerange, speed, volume, clip_settings=clip_settings)
self.material_instance = deepcopy(material)
self.material_size = (material.width, material.height)
self.effects = []
self.filters = []
self.transition = None
self.mask = None
self.background_filling = None
def add_animation(self, animation_type: Union[Intro_type, Outro_type, Group_animation_type, CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type],
duration: Optional[Union[int, str]] = None) -> "Video_segment":
"""将给定的入场/出场/组合动画添加到此片段的动画列表中
Args:
animation_type (`Intro_type`, `Outro_type`, or `Group_animation_type`): 动画类型
duration (`int` or `str`, optional): 动画持续时间, 单位为微秒. 若传入字符串则会调用`tim()`函数进行解析.
若不指定则使用动画类型定义的默认值. 理论上只适用于入场和出场动画.
"""
if duration is not None:
duration = tim(duration)
if (isinstance(animation_type, Intro_type) or isinstance(animation_type, CapCut_Intro_type)):
start = 0
duration = duration or animation_type.value.duration
elif isinstance(animation_type, Outro_type) or isinstance(animation_type, CapCut_Outro_type):
duration = duration or animation_type.value.duration
start = self.target_timerange.duration - duration
elif isinstance(animation_type, Group_animation_type) or isinstance(animation_type, CapCut_Group_animation_type):
start = 0
duration = duration or self.target_timerange.duration
else:
raise TypeError("Invalid animation type %s" % type(animation_type))
if self.animations_instance is None:
self.animations_instance = Segment_animations()
self.extra_material_refs.append(self.animations_instance.animation_id)
self.animations_instance.add_animation(Video_animation(animation_type, start, duration))
return self
def add_effect(self, effect_type: Union[Video_scene_effect_type, Video_character_effect_type],
params: Optional[List[Optional[float]]] = None) -> "Video_segment":
"""为视频片段添加一个作用于整个片段的特效
Args:
effect_type (`Video_scene_effect_type` or `Video_character_effect_type`): 特效类型
params (`List[Optional[float]]`, optional): 特效参数列表, 参数列表中未提供或为None的项使用默认值.
参数取值范围(0~100)与剪映中一致. 某个特效类型有何参数以及具体参数顺序以枚举类成员的annotation为准.
Raises:
`ValueError`: 提供的参数数量超过了该特效类型的参数数量, 或参数值超出范围.
"""
if params is not None and len(params) > len(effect_type.value.params):
raise ValueError("为音频效果 %s 传入了过多的参数" % effect_type.value.name)
effect_inst = Video_effect(effect_type, params)
self.effects.append(effect_inst)
self.extra_material_refs.append(effect_inst.global_id)
return self
def add_filter(self, filter_type: Filter_type, intensity: float = 100.0) -> "Video_segment":
"""为视频片段添加一个滤镜
Args:
filter_type (`Filter_type`): 滤镜类型
intensity (`float`, optional): 滤镜强度(0-100), 仅当所选滤镜能够调节强度时有效. 默认为100.
"""
filter_inst = Filter(filter_type.value, intensity / 100.0) # 转化为0~1范围
self.filters.append(filter_inst)
self.extra_material_refs.append(filter_inst.global_id)
return self
def add_mask(self, draft: "Script_file", mask_type: Union[Mask_type, CapCut_Mask_type], *, center_x: float = 0.0, center_y: float = 0.0, size: float = 0.5,
rotation: float = 0.0, feather: float = 0.0, invert: bool = False,
rect_width: Optional[float] = None, round_corner: Optional[float] = None) -> "Video_segment":
"""为视频片段添加蒙版
Args:
mask_type (`Mask_type`): 蒙版类型
center_x (`float`, optional): 蒙版中心点X坐标(以素材的像素为单位), 默认设置在素材中心
center_y (`float`, optional): 蒙版中心点Y坐标(以素材的像素为单位), 默认设置在素材中心
size (`float`, optional): 蒙版的"主要尺寸"(镜面的可视部分高度/圆形直径/爱心高度等), 以占素材高度的比例表示, 默认为0.5
rotation (`float`, optional): 蒙版顺时针旋转的**角度**, 默认不旋转
feather (`float`, optional): 蒙版的羽化参数, 取值范围0~100, 默认无羽化
invert (`bool`, optional): 是否反转蒙版, 默认不反转
rect_width (`float`, optional): 矩形蒙版的宽度, 仅在蒙版类型为矩形时允许设置, 以占素材宽度的比例表示, 默认与`size`相同
round_corner (`float`, optional): 矩形蒙版的圆角参数, 仅在蒙版类型为矩形时允许设置, 取值范围0~100, 默认为0
Raises:
`ValueError`: 试图添加多个蒙版或不正确地设置了`rect_width`及`round_corner`
"""
if self.mask is not None:
raise ValueError("当前片段已有蒙版, 不能再添加新的蒙版")
if (rect_width is not None or round_corner is not None) and (mask_type != Mask_type.矩形 and mask_type != CapCut_Mask_type.Rectangle):
raise ValueError("`rect_width` 以及 `round_corner` 仅在蒙版类型为矩形时允许设置")
if rect_width is None and (mask_type == Mask_type.矩形 or mask_type == CapCut_Mask_type.Rectangle):
rect_width = size
if round_corner is None:
round_corner = 0
# 获取草稿的宽高,而不是使用素材的宽高
draft_width = draft.width
draft_height = draft.height
width = rect_width or size * draft_height * mask_type.value.default_aspect_ratio / draft_width
self.mask = Mask(mask_type.value, center_x / (draft_width / 2), center_y / (draft_height / 2),
w=width, h=size, ratio=mask_type.value.default_aspect_ratio,
rot=rotation, inv=invert, feather=feather/100, round_corner=round_corner/100)
self.extra_material_refs.append(self.mask.global_id)
return self
def add_transition(self, transition_type: Union[Transition_type, CapCut_Transition_type], *, duration: Optional[Union[int, str]] = None) -> "Video_segment":
"""为视频片段添加转场, 注意转场应当添加在**前面的**片段上
Args:
transition_type (`Transition_type` or `CapCut_Transition_type`): 转场类型
duration (`int` or `str`, optional): 转场持续时间, 单位为微秒. 若传入字符串则会调用`tim()`函数进行解析. 若不指定则使用转场类型定义的默认值.
Raises:
`ValueError`: 试图添加多个转场.
"""
if self.transition is not None:
raise ValueError("当前片段已有转场, 不能再添加新的转场")
if isinstance(duration, str): duration = tim(duration)
self.transition = Transition(transition_type, duration)
self.extra_material_refs.append(self.transition.global_id)
return self
def add_background_filling(self, fill_type: Literal["blur", "color"], blur: float = 0.0625, color: str = "#00000000") -> "Video_segment":
"""为视频片段添加背景填充
注意: 背景填充仅对底层视频轨道上的片段生效
Args:
fill_type (`blur` or `color`): 填充类型, `blur`表示模糊, `color`表示颜色.
blur (`float`, optional): 模糊程度, 0.0-1.0. 仅在`fill_type`为`blur`时有效. 剪映中的四档模糊数值分别为0.0625, 0.375, 0.75和1.0, 默认为0.0625.
color (`str`, optional): 填充颜色, 格式为'#RRGGBBAA'. 仅在`fill_type`为`color`时有效.
Raises:
`ValueError`: 当前片段已有背景填充效果或`fill_type`无效.
"""
if self.background_filling is not None:
raise ValueError("当前片段已有背景填充效果")
if fill_type == "blur":
self.background_filling = BackgroundFilling("canvas_blur", blur, color)
elif fill_type == "color":
self.background_filling = BackgroundFilling("canvas_color", blur, color)
else:
raise ValueError(f"无效的背景填充类型 {fill_type}")
self.extra_material_refs.append(self.background_filling.global_id)
return self
def export_json(self) -> Dict[str, Any]:
json_dict = super().export_json()
json_dict.update({
"hdr_settings": {"intensity": 1.0, "mode": 1, "nits": 1000},
})
return json_dict
class Sticker_segment(Visual_segment):
"""安放在轨道上的一个贴纸片段"""
resource_id: str
"""贴纸资源id"""
def __init__(self, resource_id: str, target_timerange: Timerange, *, clip_settings: Optional[Clip_settings] = None):
"""根据贴纸resource_id构建一个贴纸片段, 并指定其时间信息及图像调节设置
片段创建完成后, 可通过`Script_file.add_segment`方法将其添加到轨道中
Args:
resource_id (`str`): 贴纸resource_id, 可通过`Script_file.inspect_material`从模板中获取
target_timerange (`Timerange`): 片段在轨道上的目标时间范围
clip_settings (`Clip_settings`, optional): 图像调节设置, 默认不作任何变换
"""
super().__init__(uuid.uuid4().hex, None, target_timerange, 1.0, 1.0, clip_settings=clip_settings)
self.resource_id = resource_id
def export_material(self) -> Dict[str, Any]:
"""创建极简的贴纸素材对象, 以此不再单独定义贴纸素材类"""
return {
"id": self.material_id,
"resource_id": self.resource_id,
"sticker_id": self.resource_id,
"source_platform": 1,
"type": "sticker",
}