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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""剪映自动化控制,主要与自动导出有关"""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import shutil
|
|
5
|
+
import uiautomation as uia
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Optional, Literal, Callable
|
|
9
|
+
|
|
10
|
+
from . import exceptions
|
|
11
|
+
from .exceptions import AutomationError
|
|
12
|
+
|
|
13
|
+
class ExportResolution(Enum):
|
|
14
|
+
"""导出分辨率"""
|
|
15
|
+
RES_8K = "8K"
|
|
16
|
+
RES_4K = "4K"
|
|
17
|
+
RES_2K = "2K"
|
|
18
|
+
RES_1080P = "1080P"
|
|
19
|
+
RES_720P = "720P"
|
|
20
|
+
RES_480P = "480P"
|
|
21
|
+
|
|
22
|
+
class ExportFramerate(Enum):
|
|
23
|
+
"""导出帧率"""
|
|
24
|
+
FR_24 = "24fps"
|
|
25
|
+
FR_25 = "25fps"
|
|
26
|
+
FR_30 = "30fps"
|
|
27
|
+
FR_50 = "50fps"
|
|
28
|
+
FR_60 = "60fps"
|
|
29
|
+
|
|
30
|
+
class ControlFinder:
|
|
31
|
+
"""控件查找器,封装部分与控件查找相关的逻辑"""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def desc_matcher(target_desc: str, depth: int = 2, exact: bool = False) -> Callable[[uia.Control, int], bool]:
|
|
35
|
+
"""根据full_description查找控件的匹配器"""
|
|
36
|
+
target_desc = target_desc.lower()
|
|
37
|
+
def matcher(control: uia.Control, _depth: int) -> bool:
|
|
38
|
+
if _depth != depth:
|
|
39
|
+
return False
|
|
40
|
+
full_desc: str = control.GetPropertyValue(30159).lower()
|
|
41
|
+
return (target_desc == full_desc) if exact else (target_desc in full_desc)
|
|
42
|
+
return matcher
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def class_name_matcher(class_name: str, depth: int = 1, exact: bool = False) -> Callable[[uia.Control, int], bool]:
|
|
46
|
+
"""根据ClassName查找控件的匹配器"""
|
|
47
|
+
class_name = class_name.lower()
|
|
48
|
+
def matcher(control: uia.Control, _depth: int) -> bool:
|
|
49
|
+
if _depth != depth:
|
|
50
|
+
return False
|
|
51
|
+
curr_class_name: str = control.ClassName.lower()
|
|
52
|
+
return (class_name == curr_class_name) if exact else (class_name in curr_class_name)
|
|
53
|
+
return matcher
|
|
54
|
+
|
|
55
|
+
class JianyingController:
|
|
56
|
+
"""剪映控制器"""
|
|
57
|
+
|
|
58
|
+
app: uia.WindowControl
|
|
59
|
+
"""剪映窗口"""
|
|
60
|
+
app_status: Literal["home", "edit", "pre_export"]
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
"""初始化剪映控制器, 此时剪映应该处于目录页"""
|
|
64
|
+
self.get_window()
|
|
65
|
+
|
|
66
|
+
def export_draft(self, draft_name: str, output_path: Optional[str] = None, *,
|
|
67
|
+
resolution: Optional[ExportResolution] = None,
|
|
68
|
+
framerate: Optional[ExportFramerate] = None,
|
|
69
|
+
timeout: float = 1200) -> None:
|
|
70
|
+
"""导出指定的剪映草稿, **目前仅支持剪映6及以下版本**
|
|
71
|
+
|
|
72
|
+
**注意: 需要确认有导出草稿的权限(不使用VIP功能或已开通VIP), 否则可能陷入死循环**
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
draft_name (`str`): 要导出的剪映草稿名称
|
|
76
|
+
output_path (`str`, optional): 导出路径, 支持指向文件夹或直接指向文件, 不指定则使用剪映默认路径.
|
|
77
|
+
resolution (`Export_resolution`, optional): 导出分辨率, 默认不改变剪映导出窗口中的设置.
|
|
78
|
+
framerate (`Export_framerate`, optional): 导出帧率, 默认不改变剪映导出窗口中的设置.
|
|
79
|
+
timeout (`float`, optional): 导出超时时间(秒), 默认为20分钟.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
`DraftNotFound`: 未找到指定名称的剪映草稿
|
|
83
|
+
`AutomationError`: 剪映操作失败
|
|
84
|
+
"""
|
|
85
|
+
print(f"开始导出 {draft_name} 至 {output_path}")
|
|
86
|
+
self.get_window()
|
|
87
|
+
self.switch_to_home()
|
|
88
|
+
|
|
89
|
+
# 点击对应草稿
|
|
90
|
+
draft_name_text = self.app.TextControl(
|
|
91
|
+
searchDepth=2,
|
|
92
|
+
Compare=ControlFinder.desc_matcher(f"HomePageDraftTitle:{draft_name}", exact=True)
|
|
93
|
+
)
|
|
94
|
+
if not draft_name_text.Exists(0):
|
|
95
|
+
raise exceptions.DraftNotFound(f"未找到名为{draft_name}的剪映草稿")
|
|
96
|
+
draft_btn = draft_name_text.GetParentControl()
|
|
97
|
+
assert draft_btn is not None
|
|
98
|
+
draft_btn.Click(simulateMove=False)
|
|
99
|
+
time.sleep(10)
|
|
100
|
+
self.get_window()
|
|
101
|
+
|
|
102
|
+
# 点击导出按钮
|
|
103
|
+
export_btn = self.app.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("MainWindowTitleBarExportBtn"))
|
|
104
|
+
if not export_btn.Exists(0):
|
|
105
|
+
raise AutomationError("未在编辑窗口中找到导出按钮")
|
|
106
|
+
export_btn.Click(simulateMove=False)
|
|
107
|
+
time.sleep(10)
|
|
108
|
+
self.get_window()
|
|
109
|
+
|
|
110
|
+
# 获取原始导出路径(带后缀名)
|
|
111
|
+
export_path_sib = self.app.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("ExportPath"))
|
|
112
|
+
if not export_path_sib.Exists(0):
|
|
113
|
+
raise AutomationError("未找到导出路径框")
|
|
114
|
+
export_path_text = export_path_sib.GetSiblingControl(lambda ctrl: True)
|
|
115
|
+
assert export_path_text is not None
|
|
116
|
+
export_path = export_path_text.GetPropertyValue(30159)
|
|
117
|
+
|
|
118
|
+
# 设置分辨率
|
|
119
|
+
if resolution is not None:
|
|
120
|
+
setting_group = self.app.GroupControl(searchDepth=1,
|
|
121
|
+
Compare=ControlFinder.class_name_matcher("PanelSettingsGroup_QMLTYPE"))
|
|
122
|
+
if not setting_group.Exists(0):
|
|
123
|
+
raise AutomationError("未找到导出设置组")
|
|
124
|
+
resolution_btn = setting_group.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("ExportSharpnessInput"))
|
|
125
|
+
if not resolution_btn.Exists(0.5):
|
|
126
|
+
raise AutomationError("未找到导出分辨率下拉框")
|
|
127
|
+
resolution_btn.Click(simulateMove=False)
|
|
128
|
+
time.sleep(0.5)
|
|
129
|
+
resolution_item = self.app.TextControl(
|
|
130
|
+
searchDepth=2, Compare=ControlFinder.desc_matcher(resolution.value)
|
|
131
|
+
)
|
|
132
|
+
if not resolution_item.Exists(0.5):
|
|
133
|
+
raise AutomationError(f"未找到{resolution.value}分辨率选项")
|
|
134
|
+
resolution_item.Click(simulateMove=False)
|
|
135
|
+
time.sleep(0.5)
|
|
136
|
+
|
|
137
|
+
# 设置帧率
|
|
138
|
+
if framerate is not None:
|
|
139
|
+
setting_group = self.app.GroupControl(searchDepth=1,
|
|
140
|
+
Compare=ControlFinder.class_name_matcher("PanelSettingsGroup_QMLTYPE"))
|
|
141
|
+
if not setting_group.Exists(0):
|
|
142
|
+
raise AutomationError("未找到导出设置组")
|
|
143
|
+
framerate_btn = setting_group.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("FrameRateInput"))
|
|
144
|
+
if not framerate_btn.Exists(0.5):
|
|
145
|
+
raise AutomationError("未找到导出帧率下拉框")
|
|
146
|
+
framerate_btn.Click(simulateMove=False)
|
|
147
|
+
time.sleep(0.5)
|
|
148
|
+
framerate_item = self.app.TextControl(
|
|
149
|
+
searchDepth=2, Compare=ControlFinder.desc_matcher(framerate.value)
|
|
150
|
+
)
|
|
151
|
+
if not framerate_item.Exists(0.5):
|
|
152
|
+
raise AutomationError(f"未找到{framerate.value}帧率选项")
|
|
153
|
+
framerate_item.Click(simulateMove=False)
|
|
154
|
+
time.sleep(0.5)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# 点击导出
|
|
158
|
+
export_btn = self.app.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("ExportOkBtn", exact=True))
|
|
159
|
+
if not export_btn.Exists(0):
|
|
160
|
+
raise AutomationError("未在导出窗口中找到导出按钮")
|
|
161
|
+
export_btn.Click(simulateMove=False)
|
|
162
|
+
time.sleep(5)
|
|
163
|
+
|
|
164
|
+
# 等待导出完成
|
|
165
|
+
st = time.time()
|
|
166
|
+
while True:
|
|
167
|
+
self.get_window()
|
|
168
|
+
if self.app_status != "pre_export": continue
|
|
169
|
+
|
|
170
|
+
succeed_close_btn = self.app.TextControl(searchDepth=2, Compare=ControlFinder.desc_matcher("ExportSucceedCloseBtn"))
|
|
171
|
+
if succeed_close_btn.Exists(0):
|
|
172
|
+
succeed_close_btn.Click(simulateMove=False)
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if time.time() - st > timeout:
|
|
176
|
+
raise AutomationError("导出超时, 时限为%d秒" % timeout)
|
|
177
|
+
|
|
178
|
+
time.sleep(1)
|
|
179
|
+
time.sleep(2)
|
|
180
|
+
|
|
181
|
+
# 回到目录页
|
|
182
|
+
self.get_window()
|
|
183
|
+
self.switch_to_home()
|
|
184
|
+
time.sleep(2)
|
|
185
|
+
|
|
186
|
+
# 复制导出的文件到指定目录
|
|
187
|
+
if output_path is not None:
|
|
188
|
+
shutil.move(export_path, output_path)
|
|
189
|
+
|
|
190
|
+
print(f"导出 {draft_name} 至 {output_path} 完成")
|
|
191
|
+
|
|
192
|
+
def switch_to_home(self) -> None:
|
|
193
|
+
"""切换到剪映主页"""
|
|
194
|
+
if self.app_status == "home":
|
|
195
|
+
return
|
|
196
|
+
if self.app_status != "edit":
|
|
197
|
+
raise AutomationError("仅支持从编辑模式切换到主页")
|
|
198
|
+
close_btn = self.app.GroupControl(searchDepth=1, ClassName="TitleBarButton", foundIndex=3)
|
|
199
|
+
close_btn.Click(simulateMove=False)
|
|
200
|
+
time.sleep(2)
|
|
201
|
+
self.get_window()
|
|
202
|
+
|
|
203
|
+
def get_window(self) -> None:
|
|
204
|
+
"""寻找剪映窗口并置顶"""
|
|
205
|
+
if hasattr(self, "app") and self.app.Exists(0):
|
|
206
|
+
self.app.SetTopmost(False)
|
|
207
|
+
|
|
208
|
+
self.app = uia.WindowControl(searchDepth=1, Compare=self.__jianying_window_cmp)
|
|
209
|
+
if not self.app.Exists(0):
|
|
210
|
+
raise AutomationError("剪映窗口未找到")
|
|
211
|
+
|
|
212
|
+
# 寻找可能存在的导出窗口
|
|
213
|
+
export_window = self.app.WindowControl(searchDepth=1, Name="导出")
|
|
214
|
+
if export_window.Exists(0):
|
|
215
|
+
self.app = export_window
|
|
216
|
+
self.app_status = "pre_export"
|
|
217
|
+
|
|
218
|
+
self.app.SetActive()
|
|
219
|
+
self.app.SetTopmost()
|
|
220
|
+
|
|
221
|
+
def __jianying_window_cmp(self, control: uia.WindowControl, depth: int) -> bool:
|
|
222
|
+
if control.Name != "剪映专业版":
|
|
223
|
+
return False
|
|
224
|
+
if "HomePage".lower() in control.ClassName.lower():
|
|
225
|
+
self.app_status = "home"
|
|
226
|
+
return True
|
|
227
|
+
if "MainWindow".lower() in control.ClassName.lower():
|
|
228
|
+
self.app_status = "edit"
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
|
|
6
|
+
class Keyframe:
|
|
7
|
+
"""一个关键帧(关键点), 目前只支持线性插值"""
|
|
8
|
+
|
|
9
|
+
kf_id: str
|
|
10
|
+
"""关键帧全局id, 自动生成"""
|
|
11
|
+
time_offset: int
|
|
12
|
+
"""相对于素材起始点的时间偏移量"""
|
|
13
|
+
values: List[float]
|
|
14
|
+
"""关键帧的值, 似乎一般只有一个元素"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, time_offset: int, value: float):
|
|
17
|
+
"""给定时间偏移量及关键值, 初始化关键帧"""
|
|
18
|
+
self.kf_id = uuid.uuid4().hex
|
|
19
|
+
|
|
20
|
+
self.time_offset = time_offset
|
|
21
|
+
self.values = [value]
|
|
22
|
+
|
|
23
|
+
def export_json(self) -> Dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
# 默认值
|
|
26
|
+
"curveType": "Line",
|
|
27
|
+
"graphID": "",
|
|
28
|
+
"left_control": {"x": 0.0, "y": 0.0},
|
|
29
|
+
"right_control": {"x": 0.0, "y": 0.0},
|
|
30
|
+
# 自定义属性
|
|
31
|
+
"id": self.kf_id,
|
|
32
|
+
"time_offset": self.time_offset,
|
|
33
|
+
"values": self.values
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class KeyframeProperty(Enum):
|
|
37
|
+
"""关键帧所控制的属性类型"""
|
|
38
|
+
|
|
39
|
+
position_x = "KFTypePositionX"
|
|
40
|
+
"""右移为正, 此处的数值应该为`剪映中显示的值` / `草稿宽度`, 也即单位是半个画布宽"""
|
|
41
|
+
position_y = "KFTypePositionY"
|
|
42
|
+
"""上移为正, 此处的数值应该为`剪映中显示的值` / `草稿高度`, 也即单位是半个画布高"""
|
|
43
|
+
rotation = "KFTypeRotation"
|
|
44
|
+
"""顺时针旋转的**角度**"""
|
|
45
|
+
|
|
46
|
+
scale_x = "KFTypeScaleX"
|
|
47
|
+
"""单独控制X轴缩放比例(1.0为不缩放), 与`uniform_scale`互斥"""
|
|
48
|
+
scale_y = "KFTypeScaleY"
|
|
49
|
+
"""单独控制Y轴缩放比例(1.0为不缩放), 与`uniform_scale`互斥"""
|
|
50
|
+
uniform_scale = "UNIFORM_SCALE"
|
|
51
|
+
"""同时控制X轴及Y轴缩放比例(1.0为不缩放), 与`scale_x`和`scale_y`互斥"""
|
|
52
|
+
|
|
53
|
+
alpha = "KFTypeAlpha"
|
|
54
|
+
"""不透明度, 1.0为完全不透明, 仅对`VideoSegment`有效"""
|
|
55
|
+
saturation = "KFTypeSaturation"
|
|
56
|
+
"""饱和度, 0.0为原始饱和度, 范围为-1.0到1.0, 仅对`VideoSegment`有效"""
|
|
57
|
+
contrast = "KFTypeContrast"
|
|
58
|
+
"""对比度, 0.0为原始对比度, 范围为-1.0到1.0, 仅对`VideoSegment`有效"""
|
|
59
|
+
brightness = "KFTypeBrightness"
|
|
60
|
+
"""亮度, 0.0为原始亮度, 范围为-1.0到1.0, 仅对`VideoSegment`有效"""
|
|
61
|
+
|
|
62
|
+
volume = "KFTypeVolume"
|
|
63
|
+
"""音量, 1.0为原始音量, 仅对`AudioSegment`和`VideoSegment`有效"""
|
|
64
|
+
|
|
65
|
+
class KeyframeList:
|
|
66
|
+
"""关键帧列表, 记录与某个特定属性相关的一系列关键帧"""
|
|
67
|
+
|
|
68
|
+
list_id: str
|
|
69
|
+
"""关键帧列表全局id, 自动生成"""
|
|
70
|
+
keyframe_property: KeyframeProperty
|
|
71
|
+
"""关键帧对应的属性"""
|
|
72
|
+
keyframes: List[Keyframe]
|
|
73
|
+
"""关键帧列表"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, keyframe_property: KeyframeProperty):
|
|
76
|
+
"""为给定的关键帧属性初始化关键帧列表"""
|
|
77
|
+
self.list_id = uuid.uuid4().hex
|
|
78
|
+
|
|
79
|
+
self.keyframe_property = keyframe_property
|
|
80
|
+
self.keyframes = []
|
|
81
|
+
|
|
82
|
+
def add_keyframe(self, time_offset: int, value: float):
|
|
83
|
+
"""给定时间偏移量及关键值, 向此关键帧列表中添加一个关键帧"""
|
|
84
|
+
keyframe = Keyframe(time_offset, value)
|
|
85
|
+
self.keyframes.append(keyframe)
|
|
86
|
+
self.keyframes.sort(key=lambda x: x.time_offset)
|
|
87
|
+
|
|
88
|
+
def export_json(self) -> Dict[str, Any]:
|
|
89
|
+
return {
|
|
90
|
+
"id": self.list_id,
|
|
91
|
+
"keyframe_list": [kf.export_json() for kf in self.keyframes],
|
|
92
|
+
"material_id": "",
|
|
93
|
+
"property_type": self.keyframe_property.value
|
|
94
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import uuid
|
|
3
|
+
import pymediainfo
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Literal
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
class CropSettings:
|
|
9
|
+
"""素材的裁剪设置, 各属性均在0-1之间, 注意素材的坐标原点在左上角"""
|
|
10
|
+
|
|
11
|
+
upper_left_x: float
|
|
12
|
+
upper_left_y: float
|
|
13
|
+
upper_right_x: float
|
|
14
|
+
upper_right_y: float
|
|
15
|
+
lower_left_x: float
|
|
16
|
+
lower_left_y: float
|
|
17
|
+
lower_right_x: float
|
|
18
|
+
lower_right_y: float
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, upper_left_x: float = 0.0, upper_left_y: float = 0.0,
|
|
21
|
+
upper_right_x: float = 1.0, upper_right_y: float = 0.0,
|
|
22
|
+
lower_left_x: float = 0.0, lower_left_y: float = 1.0,
|
|
23
|
+
lower_right_x: float = 1.0, lower_right_y: float = 1.0):
|
|
24
|
+
"""初始化裁剪设置, 默认参数表示不裁剪"""
|
|
25
|
+
self.upper_left_x = upper_left_x
|
|
26
|
+
self.upper_left_y = upper_left_y
|
|
27
|
+
self.upper_right_x = upper_right_x
|
|
28
|
+
self.upper_right_y = upper_right_y
|
|
29
|
+
self.lower_left_x = lower_left_x
|
|
30
|
+
self.lower_left_y = lower_left_y
|
|
31
|
+
self.lower_right_x = lower_right_x
|
|
32
|
+
self.lower_right_y = lower_right_y
|
|
33
|
+
|
|
34
|
+
def export_json(self) -> Dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"upper_left_x": self.upper_left_x,
|
|
37
|
+
"upper_left_y": self.upper_left_y,
|
|
38
|
+
"upper_right_x": self.upper_right_x,
|
|
39
|
+
"upper_right_y": self.upper_right_y,
|
|
40
|
+
"lower_left_x": self.lower_left_x,
|
|
41
|
+
"lower_left_y": self.lower_left_y,
|
|
42
|
+
"lower_right_x": self.lower_right_x,
|
|
43
|
+
"lower_right_y": self.lower_right_y
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class VideoMaterial:
|
|
47
|
+
"""本地视频素材(视频或图片), 一份素材可以在多个片段中使用"""
|
|
48
|
+
|
|
49
|
+
material_id: str
|
|
50
|
+
"""素材全局id, 自动生成"""
|
|
51
|
+
local_material_id: str
|
|
52
|
+
"""素材本地id, 意义暂不明确"""
|
|
53
|
+
material_name: str
|
|
54
|
+
"""素材名称"""
|
|
55
|
+
path: str
|
|
56
|
+
"""素材文件路径"""
|
|
57
|
+
duration: int
|
|
58
|
+
"""素材时长, 单位为微秒"""
|
|
59
|
+
height: int
|
|
60
|
+
"""素材高度"""
|
|
61
|
+
width: int
|
|
62
|
+
"""素材宽度"""
|
|
63
|
+
crop_settings: CropSettings
|
|
64
|
+
"""素材裁剪设置"""
|
|
65
|
+
material_type: Literal["video", "photo"]
|
|
66
|
+
"""素材类型: 视频或图片"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, path: str, material_name: Optional[str] = None, crop_settings: CropSettings = CropSettings()):
|
|
69
|
+
"""从指定位置加载视频(或图片)素材
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path (`str`): 素材文件路径, 支持mp4, mov, avi等常见视频文件及jpg, jpeg, png等图片文件.
|
|
73
|
+
material_name (`str`, optional): 素材名称, 如果不指定, 默认使用文件名作为素材名称.
|
|
74
|
+
crop_settings (`CropSettings`, optional): 素材裁剪设置, 默认不裁剪.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
`FileNotFoundError`: 素材文件不存在.
|
|
78
|
+
`ValueError`: 不支持的素材文件类型.
|
|
79
|
+
"""
|
|
80
|
+
path = os.path.abspath(path)
|
|
81
|
+
postfix = os.path.splitext(path)[1]
|
|
82
|
+
if not os.path.exists(path):
|
|
83
|
+
raise FileNotFoundError(f"找不到 {path}")
|
|
84
|
+
|
|
85
|
+
self.material_name = material_name if material_name else os.path.basename(path)
|
|
86
|
+
self.material_id = uuid.uuid3(uuid.NAMESPACE_DNS, self.material_name).hex
|
|
87
|
+
self.path = path
|
|
88
|
+
self.crop_settings = crop_settings
|
|
89
|
+
self.local_material_id = ""
|
|
90
|
+
|
|
91
|
+
if not pymediainfo.MediaInfo.can_parse():
|
|
92
|
+
raise ValueError(f"不支持的视频素材类型 '{postfix}'")
|
|
93
|
+
|
|
94
|
+
info: pymediainfo.MediaInfo = \
|
|
95
|
+
pymediainfo.MediaInfo.parse(path, mediainfo_options={"File_TestContinuousFileNames": "0"}) # type: ignore
|
|
96
|
+
# 有视频轨道的视为视频素材
|
|
97
|
+
if len(info.video_tracks):
|
|
98
|
+
self.material_type = "video"
|
|
99
|
+
self.duration = int(info.video_tracks[0].duration * 1e3) # type: ignore
|
|
100
|
+
self.width, self.height = info.video_tracks[0].width, info.video_tracks[0].height # type: ignore
|
|
101
|
+
# gif文件使用imageio库获取长度
|
|
102
|
+
elif postfix.lower() == ".gif":
|
|
103
|
+
import imageio
|
|
104
|
+
gif = imageio.get_reader(path)
|
|
105
|
+
|
|
106
|
+
self.material_type = "video"
|
|
107
|
+
self.duration = int(round(gif.get_meta_data()['duration'] * gif.get_length() * 1e3))
|
|
108
|
+
self.width, self.height = info.image_tracks[0].width, info.image_tracks[0].height # type: ignore
|
|
109
|
+
gif.close()
|
|
110
|
+
elif len(info.image_tracks):
|
|
111
|
+
self.material_type = "photo"
|
|
112
|
+
self.duration = 10800000000 # 相当于3h
|
|
113
|
+
self.width, self.height = info.image_tracks[0].width, info.image_tracks[0].height # type: ignore
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError(f"输入的素材文件 {path} 没有视频轨道或图片轨道")
|
|
116
|
+
|
|
117
|
+
def export_json(self) -> Dict[str, Any]:
|
|
118
|
+
video_material_json = {
|
|
119
|
+
"audio_fade": None,
|
|
120
|
+
"category_id": "",
|
|
121
|
+
"category_name": "local",
|
|
122
|
+
"check_flag": 63487,
|
|
123
|
+
"crop": self.crop_settings.export_json(),
|
|
124
|
+
"crop_ratio": "free",
|
|
125
|
+
"crop_scale": 1.0,
|
|
126
|
+
"duration": self.duration,
|
|
127
|
+
"height": self.height,
|
|
128
|
+
"id": self.material_id,
|
|
129
|
+
"local_material_id": self.local_material_id,
|
|
130
|
+
"material_id": self.material_id,
|
|
131
|
+
"material_name": self.material_name,
|
|
132
|
+
"media_path": "",
|
|
133
|
+
"path": self.path,
|
|
134
|
+
"type": self.material_type,
|
|
135
|
+
"width": self.width
|
|
136
|
+
}
|
|
137
|
+
return video_material_json
|
|
138
|
+
|
|
139
|
+
class AudioMaterial:
|
|
140
|
+
"""本地音频素材"""
|
|
141
|
+
|
|
142
|
+
material_id: str
|
|
143
|
+
"""素材全局id, 自动生成"""
|
|
144
|
+
material_name: str
|
|
145
|
+
"""素材名称"""
|
|
146
|
+
path: str
|
|
147
|
+
"""素材文件路径"""
|
|
148
|
+
|
|
149
|
+
duration: int
|
|
150
|
+
"""素材时长, 单位为微秒"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, path: str, material_name: Optional[str] = None):
|
|
153
|
+
"""从指定位置加载音频素材, 注意视频文件不应该作为音频素材使用
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
path (`str`): 素材文件路径, 支持mp3, wav等常见音频文件.
|
|
157
|
+
material_name (`str`, optional): 素材名称, 如果不指定, 默认使用文件名作为素材名称.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
`FileNotFoundError`: 素材文件不存在.
|
|
161
|
+
`ValueError`: 不支持的素材文件类型.
|
|
162
|
+
"""
|
|
163
|
+
path = os.path.abspath(path)
|
|
164
|
+
if not os.path.exists(path):
|
|
165
|
+
raise FileNotFoundError(f"找不到 {path}")
|
|
166
|
+
|
|
167
|
+
self.material_name = material_name if material_name else os.path.basename(path)
|
|
168
|
+
self.material_id = uuid.uuid3(uuid.NAMESPACE_DNS, self.material_name).hex
|
|
169
|
+
self.path = path
|
|
170
|
+
|
|
171
|
+
if not pymediainfo.MediaInfo.can_parse():
|
|
172
|
+
raise ValueError("不支持的音频素材类型 %s" % os.path.splitext(path)[1])
|
|
173
|
+
info: pymediainfo.MediaInfo = pymediainfo.MediaInfo.parse(path) # type: ignore
|
|
174
|
+
if len(info.video_tracks):
|
|
175
|
+
raise ValueError("音频素材不应包含视频轨道")
|
|
176
|
+
if not len(info.audio_tracks):
|
|
177
|
+
raise ValueError(f"给定的素材文件 {path} 没有音频轨道")
|
|
178
|
+
self.duration = int(info.audio_tracks[0].duration * 1e3) # type: ignore
|
|
179
|
+
|
|
180
|
+
def export_json(self) -> Dict[str, Any]:
|
|
181
|
+
return {
|
|
182
|
+
"app_id": 0,
|
|
183
|
+
"category_id": "",
|
|
184
|
+
"category_name": "local",
|
|
185
|
+
"check_flag": 3,
|
|
186
|
+
"copyright_limit_type": "none",
|
|
187
|
+
"duration": self.duration,
|
|
188
|
+
"effect_id": "",
|
|
189
|
+
"formula_id": "",
|
|
190
|
+
"id": self.material_id,
|
|
191
|
+
"local_material_id": self.material_id,
|
|
192
|
+
"music_id": self.material_id,
|
|
193
|
+
"name": self.material_name,
|
|
194
|
+
"path": self.path,
|
|
195
|
+
"source_platform": 0,
|
|
196
|
+
"type": "extract_music",
|
|
197
|
+
"wave_points": []
|
|
198
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""记录各种特效/音效/滤镜等的元数据"""
|
|
2
|
+
|
|
3
|
+
from .effect_meta import EffectMeta, EffectParamInstance
|
|
4
|
+
from .effect_meta import AnimationMeta
|
|
5
|
+
|
|
6
|
+
# 视频特效
|
|
7
|
+
from .video_scene_effect import VideoSceneEffectType
|
|
8
|
+
from .video_character_effect import VideoCharacterEffectType
|
|
9
|
+
|
|
10
|
+
# 视频动画
|
|
11
|
+
from .video_intro import IntroType
|
|
12
|
+
from .video_outro import OutroType
|
|
13
|
+
from .video_group_animation import GroupAnimationType
|
|
14
|
+
|
|
15
|
+
# 音频特效
|
|
16
|
+
from .audio_scene_effect import AudioSceneEffectType
|
|
17
|
+
from .tone_effect import ToneEffectType
|
|
18
|
+
from .speech_to_song import SpeechToSongType
|
|
19
|
+
|
|
20
|
+
# 文本动画
|
|
21
|
+
from .text_intro import TextIntro
|
|
22
|
+
from .text_outro import TextOutro
|
|
23
|
+
from .text_loop import TextLoopAnim
|
|
24
|
+
|
|
25
|
+
# 其它
|
|
26
|
+
from .font_meta import FontType
|
|
27
|
+
from .mask_meta import MaskType, MaskMeta
|
|
28
|
+
from .filter_meta import FilterType
|
|
29
|
+
from .transition_meta import TransitionType
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AnimationMeta",
|
|
33
|
+
"EffectMeta",
|
|
34
|
+
"EffectParamInstance",
|
|
35
|
+
"MaskType",
|
|
36
|
+
"MaskMeta",
|
|
37
|
+
"FilterType",
|
|
38
|
+
"FontType",
|
|
39
|
+
"TransitionType",
|
|
40
|
+
"IntroType",
|
|
41
|
+
"OutroType",
|
|
42
|
+
"GroupAnimationType",
|
|
43
|
+
"TextIntro",
|
|
44
|
+
"TextOutro",
|
|
45
|
+
"TextLoopAnim",
|
|
46
|
+
"AudioSceneEffectType",
|
|
47
|
+
"ToneEffectType",
|
|
48
|
+
"SpeechToSongType",
|
|
49
|
+
"VideoSceneEffectType",
|
|
50
|
+
"VideoCharacterEffectType"
|
|
51
|
+
]
|