pycapcut 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyJianYingDraft/__init__.py +276 -0
- pyJianYingDraft/animation.py +140 -0
- pyJianYingDraft/assets/__init__.py +45 -0
- pyJianYingDraft/assets/draft_content_template.json +125 -0
- pyJianYingDraft/assets/draft_meta_info.json +67 -0
- pyJianYingDraft/audio_segment.py +225 -0
- pyJianYingDraft/draft_content_template.json +119 -0
- pyJianYingDraft/draft_folder.py +153 -0
- pyJianYingDraft/effect_segment.py +36 -0
- pyJianYingDraft/exceptions.py +23 -0
- pyJianYingDraft/jianying_controller.py +230 -0
- pyJianYingDraft/keyframe.py +94 -0
- pyJianYingDraft/local_materials.py +198 -0
- pyJianYingDraft/metadata/__init__.py +51 -0
- pyJianYingDraft/metadata/animation_meta.py +689 -0
- pyJianYingDraft/metadata/audio_effect_meta.py +367 -0
- pyJianYingDraft/metadata/audio_scene_effect.py +252 -0
- pyJianYingDraft/metadata/effect_meta.py +191 -0
- pyJianYingDraft/metadata/filter_meta.py +603 -0
- pyJianYingDraft/metadata/font_meta.py +341 -0
- pyJianYingDraft/metadata/mask_meta.py +15 -0
- pyJianYingDraft/metadata/speech_to_song.py +12 -0
- pyJianYingDraft/metadata/text_intro.py +152 -0
- pyJianYingDraft/metadata/text_loop.py +105 -0
- pyJianYingDraft/metadata/text_outro.py +105 -0
- pyJianYingDraft/metadata/tone_effect.py +107 -0
- pyJianYingDraft/metadata/transition_meta.py +733 -0
- pyJianYingDraft/metadata/video_character_effect.py +1913 -0
- pyJianYingDraft/metadata/video_effect_meta.py +9619 -0
- pyJianYingDraft/metadata/video_group_animation.py +131 -0
- pyJianYingDraft/metadata/video_intro.py +103 -0
- pyJianYingDraft/metadata/video_outro.py +80 -0
- pyJianYingDraft/metadata/video_scene_effect.py +7706 -0
- pyJianYingDraft/script_file.py +808 -0
- pyJianYingDraft/segment.py +256 -0
- pyJianYingDraft/template_mode.py +221 -0
- pyJianYingDraft/text_segment.py +436 -0
- pyJianYingDraft/time_util.py +99 -0
- pyJianYingDraft/track.py +138 -0
- pyJianYingDraft/util.py +58 -0
- pyJianYingDraft/video_segment.py +526 -0
- pycapcut/__init__.py +276 -0
- pycapcut/animation.py +140 -0
- pycapcut/assets/__init__.py +45 -0
- pycapcut/assets/draft_content_template.json +132 -0
- pycapcut/assets/draft_meta_info.json +73 -0
- pycapcut/audio_segment.py +225 -0
- pycapcut/draft_folder.py +153 -0
- pycapcut/effect_segment.py +36 -0
- pycapcut/exceptions.py +23 -0
- pycapcut/jianying_controller.py +230 -0
- pycapcut/keyframe.py +94 -0
- pycapcut/local_materials.py +198 -0
- pycapcut/metadata/__init__.py +51 -0
- pycapcut/metadata/audio_scene_effect.py +252 -0
- pycapcut/metadata/effect_meta.py +191 -0
- pycapcut/metadata/filter_meta.py +603 -0
- pycapcut/metadata/font_meta.py +341 -0
- pycapcut/metadata/mask_meta.py +15 -0
- pycapcut/metadata/speech_to_song.py +12 -0
- pycapcut/metadata/text_intro.py +152 -0
- pycapcut/metadata/text_loop.py +105 -0
- pycapcut/metadata/text_outro.py +105 -0
- pycapcut/metadata/tone_effect.py +107 -0
- pycapcut/metadata/transition_meta.py +733 -0
- pycapcut/metadata/video_character_effect.py +1913 -0
- pycapcut/metadata/video_group_animation.py +131 -0
- pycapcut/metadata/video_intro.py +103 -0
- pycapcut/metadata/video_outro.py +80 -0
- pycapcut/metadata/video_scene_effect.py +7706 -0
- pycapcut/script_file.py +808 -0
- pycapcut/segment.py +256 -0
- pycapcut/template_mode.py +221 -0
- pycapcut/text_segment.py +436 -0
- pycapcut/time_util.py +99 -0
- pycapcut/track.py +138 -0
- pycapcut/util.py +58 -0
- pycapcut/video_segment.py +526 -0
- pycapcut-0.0.1.dist-info/METADATA +77 -0
- pycapcut-0.0.1.dist-info/RECORD +88 -0
- pycapcut-0.0.1.dist-info/WHEEL +5 -0
- pycapcut-0.0.1.dist-info/top_level.txt +1 -0
- tools/__init__.py +17 -0
- tools/build_release.py +270 -0
- tools/code_generator.py +247 -0
- tools/extract_all.py +256 -0
- tools/extractor_base.py +132 -0
- tools/jianying_extractor.py +262 -0
pycapcut/util.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""辅助函数,主要与模板模式有关"""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from typing import Union, Type
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
JsonExportable = Union[int, float, bool, str, List["JsonExportable"], Dict[str, "JsonExportable"]]
|
|
9
|
+
|
|
10
|
+
def provide_ctor_defaults(cls: Type) -> Dict[str, Any]:
|
|
11
|
+
"""为构造函数提供默认值,以绕开构造函数的参数限制"""
|
|
12
|
+
|
|
13
|
+
signature = inspect.signature(cls.__init__)
|
|
14
|
+
provided_defaults: Dict[str, Any] = {}
|
|
15
|
+
|
|
16
|
+
for name, param in signature.parameters.items():
|
|
17
|
+
if name == 'self': continue
|
|
18
|
+
if param.default is not inspect.Parameter.empty: continue
|
|
19
|
+
|
|
20
|
+
if param.annotation is int or param.annotation is float:
|
|
21
|
+
provided_defaults[name] = 0
|
|
22
|
+
elif param.annotation is str:
|
|
23
|
+
provided_defaults[name] = ""
|
|
24
|
+
elif param.annotation is bool:
|
|
25
|
+
provided_defaults[name] = False
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError(f"Unsupported parameter type: {param.annotation}")
|
|
28
|
+
|
|
29
|
+
return provided_defaults
|
|
30
|
+
|
|
31
|
+
def assign_attr_with_json(obj: object, attrs: List[str], json_data: Dict[str, Any]):
|
|
32
|
+
"""根据json数据赋值给指定的对象属性
|
|
33
|
+
|
|
34
|
+
若有复杂类型,则尝试调用其`import_json`方法进行构造
|
|
35
|
+
"""
|
|
36
|
+
type_hints: Dict[str, Type] = {}
|
|
37
|
+
for cls in obj.__class__.__mro__:
|
|
38
|
+
if '__annotations__' in cls.__dict__:
|
|
39
|
+
type_hints.update(cls.__annotations__)
|
|
40
|
+
|
|
41
|
+
for attr in attrs:
|
|
42
|
+
if hasattr(type_hints[attr], 'import_json'):
|
|
43
|
+
obj.__setattr__(attr, type_hints[attr].import_json(json_data[attr]))
|
|
44
|
+
else:
|
|
45
|
+
obj.__setattr__(attr, type_hints[attr](json_data[attr]))
|
|
46
|
+
|
|
47
|
+
def export_attr_to_json(obj: object, attrs: List[str]) -> Dict[str, JsonExportable]:
|
|
48
|
+
"""将对象属性导出为json数据
|
|
49
|
+
|
|
50
|
+
若有复杂类型,则尝试调用其`export_json`方法进行导出
|
|
51
|
+
"""
|
|
52
|
+
json_data: Dict[str, Any] = {}
|
|
53
|
+
for attr in attrs:
|
|
54
|
+
if hasattr(getattr(obj, attr), 'export_json'):
|
|
55
|
+
json_data[attr] = getattr(obj, attr).export_json()
|
|
56
|
+
else:
|
|
57
|
+
json_data[attr] = getattr(obj, attr)
|
|
58
|
+
return json_data
|
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""定义视频片段及其相关类
|
|
2
|
+
|
|
3
|
+
包含图像调节设置、动画效果、特效、转场等相关类
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
|
|
9
|
+
from typing import Optional, Literal, Union
|
|
10
|
+
from typing import Dict, List, Tuple, Any
|
|
11
|
+
|
|
12
|
+
from .time_util import tim, Timerange
|
|
13
|
+
from .segment import VisualSegment, ClipSettings
|
|
14
|
+
from .local_materials import VideoMaterial
|
|
15
|
+
from .animation import SegmentAnimations, VideoAnimation
|
|
16
|
+
|
|
17
|
+
from .metadata import EffectMeta, EffectParamInstance
|
|
18
|
+
from .metadata import MaskMeta, MaskType, FilterType, TransitionType
|
|
19
|
+
from .metadata import IntroType, OutroType, GroupAnimationType
|
|
20
|
+
from .metadata import VideoSceneEffectType, VideoCharacterEffectType
|
|
21
|
+
|
|
22
|
+
class Mask:
|
|
23
|
+
"""蒙版对象"""
|
|
24
|
+
|
|
25
|
+
mask_meta: MaskMeta
|
|
26
|
+
"""蒙版元数据"""
|
|
27
|
+
global_id: str
|
|
28
|
+
"""蒙版全局id, 由程序自动生成"""
|
|
29
|
+
|
|
30
|
+
center_x: float
|
|
31
|
+
"""蒙版中心x坐标, 以半素材宽为单位"""
|
|
32
|
+
center_y: float
|
|
33
|
+
"""蒙版中心y坐标, 以半素材高为单位"""
|
|
34
|
+
width: float
|
|
35
|
+
height: float
|
|
36
|
+
aspect_ratio: float
|
|
37
|
+
|
|
38
|
+
rotation: float
|
|
39
|
+
invert: bool
|
|
40
|
+
feather: float
|
|
41
|
+
"""羽化程度, 0-1"""
|
|
42
|
+
round_corner: float
|
|
43
|
+
"""矩形蒙版的圆角, 0-1"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, mask_meta: MaskMeta,
|
|
46
|
+
cx: float, cy: float, w: float, h: float,
|
|
47
|
+
ratio: float, rot: float, inv: bool, feather: float, round_corner: float):
|
|
48
|
+
self.mask_meta = mask_meta
|
|
49
|
+
self.global_id = uuid.uuid4().hex
|
|
50
|
+
|
|
51
|
+
self.center_x, self.center_y = cx, cy
|
|
52
|
+
self.width, self.height = w, h
|
|
53
|
+
self.aspect_ratio = ratio
|
|
54
|
+
|
|
55
|
+
self.rotation = rot
|
|
56
|
+
self.invert = inv
|
|
57
|
+
self.feather = feather
|
|
58
|
+
self.round_corner = round_corner
|
|
59
|
+
|
|
60
|
+
def export_json(self) -> Dict[str, Any]:
|
|
61
|
+
return {
|
|
62
|
+
"config": {
|
|
63
|
+
"aspectRatio": self.aspect_ratio,
|
|
64
|
+
"centerX": self.center_x,
|
|
65
|
+
"centerY": self.center_y,
|
|
66
|
+
"feather": self.feather,
|
|
67
|
+
"height": self.height,
|
|
68
|
+
"invert": self.invert,
|
|
69
|
+
"rotation": self.rotation,
|
|
70
|
+
"roundCorner": self.round_corner,
|
|
71
|
+
"width": self.width
|
|
72
|
+
},
|
|
73
|
+
"id": self.global_id,
|
|
74
|
+
"name": self.mask_meta.name,
|
|
75
|
+
"platform": "all",
|
|
76
|
+
"position_info": "",
|
|
77
|
+
"resource_type": self.mask_meta.resource_type,
|
|
78
|
+
"resource_id": self.mask_meta.resource_id,
|
|
79
|
+
"type": "mask"
|
|
80
|
+
# 不导出path字段
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class VideoEffect:
|
|
84
|
+
"""视频特效素材"""
|
|
85
|
+
|
|
86
|
+
name: str
|
|
87
|
+
"""特效名称"""
|
|
88
|
+
global_id: str
|
|
89
|
+
"""特效全局id, 由程序自动生成"""
|
|
90
|
+
effect_id: str
|
|
91
|
+
"""某种特效id, 由剪映本身提供"""
|
|
92
|
+
resource_id: str
|
|
93
|
+
"""资源id, 由剪映本身提供"""
|
|
94
|
+
|
|
95
|
+
effect_type: Literal["video_effect", "face_effect"]
|
|
96
|
+
apply_target_type: Literal[0, 2]
|
|
97
|
+
"""应用目标类型, 0: 片段, 2: 全局"""
|
|
98
|
+
|
|
99
|
+
adjust_params: List[EffectParamInstance]
|
|
100
|
+
|
|
101
|
+
def __init__(self, effect_meta: Union[VideoSceneEffectType, VideoCharacterEffectType],
|
|
102
|
+
params: Optional[List[Optional[float]]] = None, *,
|
|
103
|
+
apply_target_type: Literal[0, 2] = 0):
|
|
104
|
+
"""根据给定的特效元数据及参数列表构造一个视频特效对象, params的范围是0~100"""
|
|
105
|
+
|
|
106
|
+
self.name = effect_meta.value.name
|
|
107
|
+
self.global_id = uuid.uuid4().hex
|
|
108
|
+
self.effect_id = effect_meta.value.effect_id
|
|
109
|
+
self.resource_id = effect_meta.value.resource_id
|
|
110
|
+
self.adjust_params = []
|
|
111
|
+
|
|
112
|
+
if isinstance(effect_meta, VideoSceneEffectType):
|
|
113
|
+
self.effect_type = "video_effect"
|
|
114
|
+
elif isinstance(effect_meta, VideoCharacterEffectType):
|
|
115
|
+
self.effect_type = "face_effect"
|
|
116
|
+
else:
|
|
117
|
+
raise TypeError("Invalid effect meta type %s" % type(effect_meta))
|
|
118
|
+
self.apply_target_type = apply_target_type
|
|
119
|
+
|
|
120
|
+
self.adjust_params = effect_meta.value.parse_params(params)
|
|
121
|
+
|
|
122
|
+
def export_json(self) -> Dict[str, Any]:
|
|
123
|
+
return {
|
|
124
|
+
"adjust_params": [param.export_json() for param in self.adjust_params],
|
|
125
|
+
"apply_target_type": self.apply_target_type,
|
|
126
|
+
"apply_time_range": None,
|
|
127
|
+
"category_id": "", # 一律设为空
|
|
128
|
+
"category_name": "", # 一律设为空
|
|
129
|
+
"common_keyframes": [],
|
|
130
|
+
"disable_effect_faces": [],
|
|
131
|
+
"effect_id": self.effect_id,
|
|
132
|
+
"formula_id": "",
|
|
133
|
+
"id": self.global_id,
|
|
134
|
+
"name": self.name,
|
|
135
|
+
"platform": "all",
|
|
136
|
+
"render_index": 11000,
|
|
137
|
+
"resource_id": self.resource_id,
|
|
138
|
+
"source_platform": 0,
|
|
139
|
+
"time_range": None,
|
|
140
|
+
"track_render_index": 0,
|
|
141
|
+
"type": self.effect_type,
|
|
142
|
+
"value": 1.0,
|
|
143
|
+
"version": ""
|
|
144
|
+
# 不导出path、request_id和algorithm_artifact_path字段
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class Filter:
|
|
148
|
+
"""滤镜素材"""
|
|
149
|
+
|
|
150
|
+
global_id: str
|
|
151
|
+
"""滤镜全局id, 由程序自动生成"""
|
|
152
|
+
|
|
153
|
+
effect_meta: EffectMeta
|
|
154
|
+
"""滤镜的元数据"""
|
|
155
|
+
intensity: float
|
|
156
|
+
"""滤镜强度(滤镜的唯一参数)"""
|
|
157
|
+
|
|
158
|
+
apply_target_type: Literal[0, 2]
|
|
159
|
+
"""应用目标类型, 0: 片段, 2: 全局"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, meta: EffectMeta, intensity: float, *,
|
|
162
|
+
apply_target_type: Literal[0, 2] = 0):
|
|
163
|
+
"""根据给定的滤镜元数据及强度构造滤镜素材对象"""
|
|
164
|
+
|
|
165
|
+
self.global_id = uuid.uuid4().hex
|
|
166
|
+
self.effect_meta = meta
|
|
167
|
+
self.intensity = intensity
|
|
168
|
+
self.apply_target_type = apply_target_type
|
|
169
|
+
|
|
170
|
+
def export_json(self) -> Dict[str, Any]:
|
|
171
|
+
return {
|
|
172
|
+
"adjust_params": [],
|
|
173
|
+
"algorithm_artifact_path": "",
|
|
174
|
+
"apply_target_type": self.apply_target_type,
|
|
175
|
+
"bloom_params": None,
|
|
176
|
+
"category_id": "", # 一律设为空
|
|
177
|
+
"category_name": "", # 一律设为空
|
|
178
|
+
"color_match_info": {
|
|
179
|
+
"source_feature_path": "",
|
|
180
|
+
"target_feature_path": "",
|
|
181
|
+
"target_image_path": ""
|
|
182
|
+
},
|
|
183
|
+
"effect_id": self.effect_meta.effect_id,
|
|
184
|
+
"enable_skin_tone_correction": False,
|
|
185
|
+
"exclusion_group": [],
|
|
186
|
+
"face_adjust_params": [],
|
|
187
|
+
"formula_id": "",
|
|
188
|
+
"id": self.global_id,
|
|
189
|
+
"intensity_key": "",
|
|
190
|
+
"multi_language_current": "",
|
|
191
|
+
"name": self.effect_meta.name,
|
|
192
|
+
"panel_id": "",
|
|
193
|
+
"platform": "all",
|
|
194
|
+
"resource_id": self.effect_meta.resource_id,
|
|
195
|
+
"source_platform": 1,
|
|
196
|
+
"sub_type": "none",
|
|
197
|
+
"time_range": None,
|
|
198
|
+
"type": "filter",
|
|
199
|
+
"value": self.intensity,
|
|
200
|
+
"version": ""
|
|
201
|
+
# 不导出path和request_id
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
class Transition:
|
|
205
|
+
"""转场对象"""
|
|
206
|
+
|
|
207
|
+
name: str
|
|
208
|
+
"""转场名称"""
|
|
209
|
+
global_id: str
|
|
210
|
+
"""转场全局id, 由程序自动生成"""
|
|
211
|
+
effect_id: str
|
|
212
|
+
"""转场效果id, 由剪映本身提供"""
|
|
213
|
+
resource_id: str
|
|
214
|
+
"""资源id, 由剪映本身提供"""
|
|
215
|
+
|
|
216
|
+
duration: int
|
|
217
|
+
"""转场持续时间, 单位为微秒"""
|
|
218
|
+
is_overlap: bool
|
|
219
|
+
"""是否与上一个片段重叠(?)"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, effect_meta: TransitionType, duration: Optional[int] = None):
|
|
222
|
+
"""根据给定的转场元数据及持续时间构造一个转场对象"""
|
|
223
|
+
self.name = effect_meta.value.name
|
|
224
|
+
self.global_id = uuid.uuid4().hex
|
|
225
|
+
self.effect_id = effect_meta.value.effect_id
|
|
226
|
+
self.resource_id = effect_meta.value.resource_id
|
|
227
|
+
|
|
228
|
+
self.duration = duration if duration is not None else effect_meta.value.default_duration
|
|
229
|
+
self.is_overlap = effect_meta.value.is_overlap
|
|
230
|
+
|
|
231
|
+
def export_json(self) -> Dict[str, Any]:
|
|
232
|
+
return {
|
|
233
|
+
"category_id": "", # 一律设为空
|
|
234
|
+
"category_name": "", # 一律设为空
|
|
235
|
+
"duration": self.duration,
|
|
236
|
+
"effect_id": self.effect_id,
|
|
237
|
+
"id": self.global_id,
|
|
238
|
+
"is_overlap": self.is_overlap,
|
|
239
|
+
"name": self.name,
|
|
240
|
+
"platform": "all",
|
|
241
|
+
"resource_id": self.resource_id,
|
|
242
|
+
"type": "transition"
|
|
243
|
+
# 不导出path和request_id字段
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class BackgroundFilling:
|
|
247
|
+
"""背景填充对象"""
|
|
248
|
+
|
|
249
|
+
global_id: str
|
|
250
|
+
"""背景填充全局id, 由程序自动生成"""
|
|
251
|
+
fill_type: Literal["canvas_blur", "canvas_color"]
|
|
252
|
+
"""背景填充类型"""
|
|
253
|
+
blur: float
|
|
254
|
+
"""模糊程度, 0-1"""
|
|
255
|
+
color: str
|
|
256
|
+
"""背景颜色, 格式为'#RRGGBBAA'"""
|
|
257
|
+
|
|
258
|
+
def __init__(self, fill_type: Literal["canvas_blur", "canvas_color"], blur: float, color: str):
|
|
259
|
+
self.global_id = uuid.uuid4().hex
|
|
260
|
+
self.fill_type = fill_type
|
|
261
|
+
self.blur = blur
|
|
262
|
+
self.color = color
|
|
263
|
+
|
|
264
|
+
def export_json(self) -> Dict[str, Any]:
|
|
265
|
+
return {
|
|
266
|
+
"id": self.global_id,
|
|
267
|
+
"type": self.fill_type,
|
|
268
|
+
"blur": self.blur,
|
|
269
|
+
"color": self.color,
|
|
270
|
+
"source_platform": 0,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
class VideoSegment(VisualSegment):
|
|
274
|
+
"""安放在轨道上的一个视频/图片片段"""
|
|
275
|
+
|
|
276
|
+
material_instance: VideoMaterial
|
|
277
|
+
"""素材实例"""
|
|
278
|
+
material_size: Tuple[int, int]
|
|
279
|
+
"""素材尺寸"""
|
|
280
|
+
|
|
281
|
+
effects: List[VideoEffect]
|
|
282
|
+
"""特效列表
|
|
283
|
+
|
|
284
|
+
在放入轨道时自动添加到素材列表中
|
|
285
|
+
"""
|
|
286
|
+
filters: List[Filter]
|
|
287
|
+
"""滤镜列表
|
|
288
|
+
|
|
289
|
+
在放入轨道时自动添加到素材列表中
|
|
290
|
+
"""
|
|
291
|
+
mask: Optional[Mask]
|
|
292
|
+
"""蒙版实例, 可能为空
|
|
293
|
+
|
|
294
|
+
在放入轨道时自动添加到素材列表中
|
|
295
|
+
"""
|
|
296
|
+
transition: Optional[Transition]
|
|
297
|
+
"""转场实例, 可能为空
|
|
298
|
+
|
|
299
|
+
在放入轨道时自动添加到素材列表中
|
|
300
|
+
"""
|
|
301
|
+
background_filling: Optional[BackgroundFilling]
|
|
302
|
+
"""背景填充实例, 可能为空
|
|
303
|
+
|
|
304
|
+
在放入轨道时自动添加到素材列表中
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
def __init__(self, material: Union[VideoMaterial, str], target_timerange: Timerange, *,
|
|
308
|
+
source_timerange: Optional[Timerange] = None, speed: Optional[float] = None, volume: float = 1.0,
|
|
309
|
+
clip_settings: Optional[ClipSettings] = None):
|
|
310
|
+
"""利用给定的视频/图片素材构建一个轨道片段, 并指定其时间信息及图像调节设置
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
material (`VideoMaterial` or `str`): 素材实例或素材路径, 若为路径则自动构造素材实例(此时不能指定`cropSettings`参数)
|
|
314
|
+
target_timerange (`Timerange`): 片段在轨道上的目标时间范围
|
|
315
|
+
source_timerange (`Timerange`, optional): 截取的素材片段的时间范围, 默认从开头根据`speed`截取与`target_timerange`等长的一部分
|
|
316
|
+
speed (`float`, optional): 播放速度, 默认为1.0. 此项与`source_timerange`同时指定时, 将覆盖`target_timerange`中的时长
|
|
317
|
+
volume (`float`, optional): 音量, 默认为1.0
|
|
318
|
+
clip_settings (`ClipSettings`, optional): 图像调节设置, 默认不作任何变换
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
`ValueError`: 指定的或计算出的`source_timerange`超出了素材的时长范围
|
|
322
|
+
"""
|
|
323
|
+
if isinstance(material, str):
|
|
324
|
+
material = VideoMaterial(material)
|
|
325
|
+
|
|
326
|
+
if source_timerange is not None and speed is not None:
|
|
327
|
+
target_timerange = Timerange(target_timerange.start, round(source_timerange.duration / speed))
|
|
328
|
+
elif source_timerange is not None and speed is None:
|
|
329
|
+
speed = source_timerange.duration / target_timerange.duration
|
|
330
|
+
else: # source_timerange is None
|
|
331
|
+
speed = speed if speed is not None else 1.0
|
|
332
|
+
source_timerange = Timerange(0, round(target_timerange.duration * speed))
|
|
333
|
+
|
|
334
|
+
if source_timerange.end > material.duration:
|
|
335
|
+
raise ValueError(f"截取的素材时间范围 {source_timerange} 超出了素材时长({material.duration})")
|
|
336
|
+
|
|
337
|
+
super().__init__(material.material_id, source_timerange, target_timerange, speed, volume, clip_settings=clip_settings)
|
|
338
|
+
|
|
339
|
+
self.material_instance = deepcopy(material)
|
|
340
|
+
self.material_size = (material.width, material.height)
|
|
341
|
+
self.effects = []
|
|
342
|
+
self.filters = []
|
|
343
|
+
self.transition = None
|
|
344
|
+
self.mask = None
|
|
345
|
+
self.background_filling = None
|
|
346
|
+
|
|
347
|
+
def add_animation(self, animation_type: Union[IntroType, OutroType, GroupAnimationType],
|
|
348
|
+
duration: Optional[Union[int, str]] = None) -> "VideoSegment":
|
|
349
|
+
"""将给定的入场/出场/组合动画添加到此片段的动画列表中
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
animation_type (`IntroType`, `OutroType`, or `GroupAnimationType`): 动画类型
|
|
353
|
+
duration (`int` or `str`, optional): 动画持续时间, 单位为微秒. 若传入字符串则会调用`tim()`函数进行解析.
|
|
354
|
+
若不指定则使用动画类型定义的默认值. 理论上只适用于入场和出场动画.
|
|
355
|
+
"""
|
|
356
|
+
if duration is not None:
|
|
357
|
+
duration = tim(duration)
|
|
358
|
+
if isinstance(animation_type, IntroType):
|
|
359
|
+
start = 0
|
|
360
|
+
duration = duration or animation_type.value.duration
|
|
361
|
+
elif isinstance(animation_type, OutroType):
|
|
362
|
+
duration = duration or animation_type.value.duration
|
|
363
|
+
start = self.target_timerange.duration - duration
|
|
364
|
+
elif isinstance(animation_type, GroupAnimationType):
|
|
365
|
+
start = 0
|
|
366
|
+
duration = duration or self.target_timerange.duration
|
|
367
|
+
else:
|
|
368
|
+
raise TypeError("Invalid animation type %s" % type(animation_type))
|
|
369
|
+
|
|
370
|
+
if self.animations_instance is None:
|
|
371
|
+
self.animations_instance = SegmentAnimations()
|
|
372
|
+
self.extra_material_refs.append(self.animations_instance.animation_id)
|
|
373
|
+
|
|
374
|
+
self.animations_instance.add_animation(VideoAnimation(animation_type, start, duration))
|
|
375
|
+
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
def add_effect(self, effect_type: Union[VideoSceneEffectType, VideoCharacterEffectType],
|
|
379
|
+
params: Optional[List[Optional[float]]] = None) -> "VideoSegment":
|
|
380
|
+
"""为视频片段添加一个作用于整个片段的特效
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
effect_type (`VideoSceneEffectType` or `VideoCharacterEffectType`): 特效类型
|
|
384
|
+
params (`List[Optional[float]]`, optional): 特效参数列表, 参数列表中未提供或为None的项使用默认值.
|
|
385
|
+
参数取值范围(0~100)与剪映中一致. 某个特效类型有何参数以及具体参数顺序以枚举类成员的annotation为准.
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
`ValueError`: 提供的参数数量超过了该特效类型的参数数量, 或参数值超出范围.
|
|
389
|
+
"""
|
|
390
|
+
if params is not None and len(params) > len(effect_type.value.params):
|
|
391
|
+
raise ValueError("为音频效果 %s 传入了过多的参数" % effect_type.value.name)
|
|
392
|
+
|
|
393
|
+
effect_inst = VideoEffect(effect_type, params)
|
|
394
|
+
self.effects.append(effect_inst)
|
|
395
|
+
self.extra_material_refs.append(effect_inst.global_id)
|
|
396
|
+
|
|
397
|
+
return self
|
|
398
|
+
|
|
399
|
+
def add_filter(self, filter_type: FilterType, intensity: float = 100.0) -> "VideoSegment":
|
|
400
|
+
"""为视频片段添加一个滤镜
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
filter_type (`FilterType`): 滤镜类型
|
|
404
|
+
intensity (`float`, optional): 滤镜强度(0-100), 仅当所选滤镜能够调节强度时有效. 默认为100.
|
|
405
|
+
"""
|
|
406
|
+
filter_inst = Filter(filter_type.value, intensity / 100.0) # 转化为0~1范围
|
|
407
|
+
self.filters.append(filter_inst)
|
|
408
|
+
self.extra_material_refs.append(filter_inst.global_id)
|
|
409
|
+
|
|
410
|
+
return self
|
|
411
|
+
|
|
412
|
+
def add_mask(self, mask_type: MaskType, *, center_x: float = 0.0, center_y: float = 0.0, size: float = 0.5,
|
|
413
|
+
rotation: float = 0.0, feather: float = 0.0, invert: bool = False,
|
|
414
|
+
rect_width: Optional[float] = None, round_corner: Optional[float] = None) -> "VideoSegment":
|
|
415
|
+
"""为视频片段添加蒙版
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
mask_type (`MaskType`): 蒙版类型
|
|
419
|
+
center_x (`float`, optional): 蒙版中心点X坐标(以素材的像素为单位), 默认设置在素材中心
|
|
420
|
+
center_y (`float`, optional): 蒙版中心点Y坐标(以素材的像素为单位), 默认设置在素材中心
|
|
421
|
+
size (`float`, optional): 蒙版的"主要尺寸"(镜面的可视部分高度/圆形直径/爱心高度等), 以占素材高度的比例表示, 默认为0.5
|
|
422
|
+
rotation (`float`, optional): 蒙版顺时针旋转的**角度**, 默认不旋转
|
|
423
|
+
feather (`float`, optional): 蒙版的羽化参数, 取值范围0~100, 默认无羽化
|
|
424
|
+
invert (`bool`, optional): 是否反转蒙版, 默认不反转
|
|
425
|
+
rect_width (`float`, optional): 矩形蒙版的宽度, 仅在蒙版类型为矩形时允许设置, 以占素材宽度的比例表示, 默认与`size`相同
|
|
426
|
+
round_corner (`float`, optional): 矩形蒙版的圆角参数, 仅在蒙版类型为矩形时允许设置, 取值范围0~100, 默认为0
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
`ValueError`: 试图添加多个蒙版或不正确地设置了`rect_width`及`round_corner`
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
if self.mask is not None:
|
|
433
|
+
raise ValueError("当前片段已有蒙版, 不能再添加新的蒙版")
|
|
434
|
+
if (rect_width is not None or round_corner is not None) and mask_type != MaskType.矩形:
|
|
435
|
+
raise ValueError("`rect_width` 以及 `round_corner` 仅在蒙版类型为矩形时允许设置")
|
|
436
|
+
if rect_width is None and mask_type == MaskType.矩形:
|
|
437
|
+
rect_width = size
|
|
438
|
+
if round_corner is None:
|
|
439
|
+
round_corner = 0
|
|
440
|
+
|
|
441
|
+
width = rect_width or size * self.material_size[1] * mask_type.value.default_aspect_ratio / self.material_size[0]
|
|
442
|
+
self.mask = Mask(mask_type.value, center_x / (self.material_size[0] / 2), center_y / (self.material_size[1] / 2),
|
|
443
|
+
w=width, h=size, ratio=mask_type.value.default_aspect_ratio,
|
|
444
|
+
rot=rotation, inv=invert, feather=feather/100, round_corner=round_corner/100)
|
|
445
|
+
self.extra_material_refs.append(self.mask.global_id)
|
|
446
|
+
return self
|
|
447
|
+
|
|
448
|
+
def add_transition(self, transition_type: TransitionType, *, duration: Optional[Union[int, str]] = None) -> "VideoSegment":
|
|
449
|
+
"""为视频片段添加转场, 注意转场应当添加在**前面的**片段上
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
transition_type (`TransitionType`): 转场类型
|
|
453
|
+
duration (`int` or `str`, optional): 转场持续时间, 单位为微秒. 若传入字符串则会调用`tim()`函数进行解析. 若不指定则使用转场类型定义的默认值.
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
`ValueError`: 试图添加多个转场.
|
|
457
|
+
"""
|
|
458
|
+
if self.transition is not None:
|
|
459
|
+
raise ValueError("当前片段已有转场, 不能再添加新的转场")
|
|
460
|
+
if isinstance(duration, str): duration = tim(duration)
|
|
461
|
+
|
|
462
|
+
self.transition = Transition(transition_type, duration)
|
|
463
|
+
self.extra_material_refs.append(self.transition.global_id)
|
|
464
|
+
return self
|
|
465
|
+
|
|
466
|
+
def add_background_filling(self, fill_type: Literal["blur", "color"], blur: float = 0.0625, color: str = "#00000000") -> "VideoSegment":
|
|
467
|
+
"""为视频片段添加背景填充
|
|
468
|
+
|
|
469
|
+
注意: 背景填充仅对底层视频轨道上的片段生效
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
fill_type (`blur` or `color`): 填充类型, `blur`表示模糊, `color`表示颜色.
|
|
473
|
+
blur (`float`, optional): 模糊程度, 0.0-1.0. 仅在`fill_type`为`blur`时有效. 剪映中的四档模糊数值分别为0.0625, 0.375, 0.75和1.0, 默认为0.0625.
|
|
474
|
+
color (`str`, optional): 填充颜色, 格式为'#RRGGBBAA'. 仅在`fill_type`为`color`时有效.
|
|
475
|
+
|
|
476
|
+
Raises:
|
|
477
|
+
`ValueError`: 当前片段已有背景填充效果或`fill_type`无效.
|
|
478
|
+
"""
|
|
479
|
+
if self.background_filling is not None:
|
|
480
|
+
raise ValueError("当前片段已有背景填充效果")
|
|
481
|
+
|
|
482
|
+
if fill_type == "blur":
|
|
483
|
+
self.background_filling = BackgroundFilling("canvas_blur", blur, color)
|
|
484
|
+
elif fill_type == "color":
|
|
485
|
+
self.background_filling = BackgroundFilling("canvas_color", blur, color)
|
|
486
|
+
else:
|
|
487
|
+
raise ValueError(f"无效的背景填充类型 {fill_type}")
|
|
488
|
+
|
|
489
|
+
self.extra_material_refs.append(self.background_filling.global_id)
|
|
490
|
+
return self
|
|
491
|
+
|
|
492
|
+
def export_json(self) -> Dict[str, Any]:
|
|
493
|
+
json_dict = super().export_json()
|
|
494
|
+
json_dict.update({
|
|
495
|
+
"hdr_settings": {"intensity": 1.0, "mode": 1, "nits": 1000},
|
|
496
|
+
})
|
|
497
|
+
return json_dict
|
|
498
|
+
|
|
499
|
+
class StickerSegment(VisualSegment):
|
|
500
|
+
"""安放在轨道上的一个贴纸片段"""
|
|
501
|
+
|
|
502
|
+
resource_id: str
|
|
503
|
+
"""贴纸资源id"""
|
|
504
|
+
|
|
505
|
+
def __init__(self, resource_id: str, target_timerange: Timerange, *, clip_settings: Optional[ClipSettings] = None):
|
|
506
|
+
"""根据贴纸resource_id构建一个贴纸片段, 并指定其时间信息及图像调节设置
|
|
507
|
+
|
|
508
|
+
片段创建完成后, 可通过`ScriptFile.add_segment`方法将其添加到轨道中
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
resource_id (`str`): 贴纸resource_id, 可通过`ScriptFile.inspect_material`从模板中获取
|
|
512
|
+
target_timerange (`Timerange`): 片段在轨道上的目标时间范围
|
|
513
|
+
clip_settings (`ClipSettings`, optional): 图像调节设置, 默认不作任何变换
|
|
514
|
+
"""
|
|
515
|
+
super().__init__(uuid.uuid4().hex, None, target_timerange, 1.0, 1.0, clip_settings=clip_settings)
|
|
516
|
+
self.resource_id = resource_id
|
|
517
|
+
|
|
518
|
+
def export_material(self) -> Dict[str, Any]:
|
|
519
|
+
"""创建极简的贴纸素材对象, 以此不再单独定义贴纸素材类"""
|
|
520
|
+
return {
|
|
521
|
+
"id": self.material_id,
|
|
522
|
+
"resource_id": self.resource_id,
|
|
523
|
+
"sticker_id": self.resource_id,
|
|
524
|
+
"source_platform": 1,
|
|
525
|
+
"type": "sticker",
|
|
526
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pycapcut
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: 轻量、灵活、易上手的Python CapCut草稿生成及导出工具,构建全自动化视频剪辑/混剪流水线
|
|
5
|
+
Home-page: https://github.com/GuanYixuan/pycapcut
|
|
6
|
+
Author: gary318
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Topic :: Multimedia :: Video
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pymediainfo
|
|
13
|
+
Requires-Dist: imageio
|
|
14
|
+
Requires-Dist: uiautomation>=2; sys_platform == "win32"
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
### 轻量、灵活、易上手的Python CapCut草稿生成及导出工具,构建全自动视频剪辑/混剪流水线!
|
|
25
|
+
|
|
26
|
+
> 🧪 本项目正在从[PyJianYingDraft](https://github.com/GuanYixuan/pyJianYingDraft)进行迁移,欢迎⭐️此项目保持关注!
|
|
27
|
+
|
|
28
|
+
> 📢 欢迎加入[Discord服务器](https://discord.gg/WfHgGQvhyW)进行用法或新功能的讨论
|
|
29
|
+
|
|
30
|
+
## 使用思路
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
# 功能清单
|
|
34
|
+
|
|
35
|
+
### 模板模式
|
|
36
|
+
- ☑️ [加载](#加载模板)(未加密的)`draft_content.json`文件作为模板
|
|
37
|
+
- ☑️ [替换音视频片段的素材](#根据名称替换素材)
|
|
38
|
+
- ☑️ [修改文本片段的文本内容](#替换文本片段的内容)
|
|
39
|
+
- ☑️ [将模板草稿中的音视频/文本轨道整体导入到另一草稿中](#导入模板草稿中的轨道)
|
|
40
|
+
- ☑️ [提取模板中出现的贴纸/气泡/花字等元信息](#提取素材元数据)
|
|
41
|
+
|
|
42
|
+
### 批量导出
|
|
43
|
+
> ⚠️ 本模块正在迁移中,若有兴趣可参见[PyJianYingDraft中相应部分](https://github.com/GuanYixuan/pyJianYingDraft?tab=readme-ov-file#%E6%89%B9%E9%87%8F%E5%AF%BC%E5%87%BA%E8%8D%89%E7%A8%BF)
|
|
44
|
+
|
|
45
|
+
- ☑️ 控制剪映打开指定草稿
|
|
46
|
+
- ☑️ [导出草稿至指定位置](#批量导出草稿)
|
|
47
|
+
- ☑️ 调节导出分辨率和帧率
|
|
48
|
+
|
|
49
|
+
### 视频与图片
|
|
50
|
+
> ⚠️ 本模块与特效/滤镜/动画等相关的功能正在迁移中,若有兴趣可参见[PyJianYingDraft中相应部分](https://github.com/GuanYixuan/pyJianYingDraft?tab=readme-ov-file#%E8%A7%86%E9%A2%91%E4%B8%8E%E5%9B%BE%E7%89%87)
|
|
51
|
+
|
|
52
|
+
- ☑️ 添加本地视频/图片素材,并[自定义片段的时间、持续时长或播放速度](#素材截取与整体变速)
|
|
53
|
+
- ☑️ [视频整体调节](#视频整体调节)(旋转、缩放、亮度等)以及[关键帧生成](#关键帧)
|
|
54
|
+
- ☑️ 视频片段的[入场/出场/组合动画](#添加片段动画)
|
|
55
|
+
- ☑️ 添加[蒙版](#蒙版)、[片段特效](#添加片段特效)和[滤镜](#添加片段滤镜)
|
|
56
|
+
- ☑️ 视频背景填充[(示例代码)](demo.py)
|
|
57
|
+
### 贴纸
|
|
58
|
+
- ☑️ 根据元信息[添加贴纸](#提取素材元数据)
|
|
59
|
+
- ☑️ 贴纸的[关键帧](#关键帧)生成
|
|
60
|
+
### 音频
|
|
61
|
+
- ☑️ 添加本地音频素材,并[自定义片段的时间、持续时长或播放速度](#素材截取与整体变速)
|
|
62
|
+
- ☑️ 调整淡入淡出时长[(示例代码)](demo.py),调整音量[(示例代码)](demo.py)及其[关键帧](#关键帧)
|
|
63
|
+
- ☑️ 添加音频片段的[场景音效果](#添加片段特效),并设置参数
|
|
64
|
+
### 轨道
|
|
65
|
+
- ☑️ [添加轨道](#多轨道操作)以及[将片段添加到指定轨道](#多轨道操作)
|
|
66
|
+
- ☑️ 自定义视频/滤镜/特效轨道的[层级关系](#多轨道操作)
|
|
67
|
+
### 特效、滤镜和转场
|
|
68
|
+
- ☑️ 吸附于片段上的[特效](#添加片段特效)、[滤镜](#添加片段滤镜)和[动画](#添加片段动画)
|
|
69
|
+
- ☑️ 位于[独立轨道的特效和滤镜](#独立轨道上的特效和滤镜)
|
|
70
|
+
- ☑️ 添加转场[(示例代码)](demo.py),并自定义其时长
|
|
71
|
+
### 文本及字幕
|
|
72
|
+
- ☑️ [添加文本、设置字体及样式](#添加文本)、修改文本片段的[位置及旋转设置](#视频整体调节)
|
|
73
|
+
- ☑️ 文本的[关键帧](#关键帧)以及[动画](#添加片段动画)
|
|
74
|
+
- ☑️ 文字描边和文字背景
|
|
75
|
+
- ☑️ 文字气泡效果和花字效果[(示例代码)](demo.py)
|
|
76
|
+
- ☑️ 文本[自动换行](#文本自动换行),支持设置最大行宽
|
|
77
|
+
- ☑️ [导入`.srt`文件](#导入字幕)生成字幕并批量设置格式
|