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.
Files changed (88) hide show
  1. pyJianYingDraft/__init__.py +276 -0
  2. pyJianYingDraft/animation.py +140 -0
  3. pyJianYingDraft/assets/__init__.py +45 -0
  4. pyJianYingDraft/assets/draft_content_template.json +125 -0
  5. pyJianYingDraft/assets/draft_meta_info.json +67 -0
  6. pyJianYingDraft/audio_segment.py +225 -0
  7. pyJianYingDraft/draft_content_template.json +119 -0
  8. pyJianYingDraft/draft_folder.py +153 -0
  9. pyJianYingDraft/effect_segment.py +36 -0
  10. pyJianYingDraft/exceptions.py +23 -0
  11. pyJianYingDraft/jianying_controller.py +230 -0
  12. pyJianYingDraft/keyframe.py +94 -0
  13. pyJianYingDraft/local_materials.py +198 -0
  14. pyJianYingDraft/metadata/__init__.py +51 -0
  15. pyJianYingDraft/metadata/animation_meta.py +689 -0
  16. pyJianYingDraft/metadata/audio_effect_meta.py +367 -0
  17. pyJianYingDraft/metadata/audio_scene_effect.py +252 -0
  18. pyJianYingDraft/metadata/effect_meta.py +191 -0
  19. pyJianYingDraft/metadata/filter_meta.py +603 -0
  20. pyJianYingDraft/metadata/font_meta.py +341 -0
  21. pyJianYingDraft/metadata/mask_meta.py +15 -0
  22. pyJianYingDraft/metadata/speech_to_song.py +12 -0
  23. pyJianYingDraft/metadata/text_intro.py +152 -0
  24. pyJianYingDraft/metadata/text_loop.py +105 -0
  25. pyJianYingDraft/metadata/text_outro.py +105 -0
  26. pyJianYingDraft/metadata/tone_effect.py +107 -0
  27. pyJianYingDraft/metadata/transition_meta.py +733 -0
  28. pyJianYingDraft/metadata/video_character_effect.py +1913 -0
  29. pyJianYingDraft/metadata/video_effect_meta.py +9619 -0
  30. pyJianYingDraft/metadata/video_group_animation.py +131 -0
  31. pyJianYingDraft/metadata/video_intro.py +103 -0
  32. pyJianYingDraft/metadata/video_outro.py +80 -0
  33. pyJianYingDraft/metadata/video_scene_effect.py +7706 -0
  34. pyJianYingDraft/script_file.py +808 -0
  35. pyJianYingDraft/segment.py +256 -0
  36. pyJianYingDraft/template_mode.py +221 -0
  37. pyJianYingDraft/text_segment.py +436 -0
  38. pyJianYingDraft/time_util.py +99 -0
  39. pyJianYingDraft/track.py +138 -0
  40. pyJianYingDraft/util.py +58 -0
  41. pyJianYingDraft/video_segment.py +526 -0
  42. pycapcut/__init__.py +276 -0
  43. pycapcut/animation.py +140 -0
  44. pycapcut/assets/__init__.py +45 -0
  45. pycapcut/assets/draft_content_template.json +132 -0
  46. pycapcut/assets/draft_meta_info.json +73 -0
  47. pycapcut/audio_segment.py +225 -0
  48. pycapcut/draft_folder.py +153 -0
  49. pycapcut/effect_segment.py +36 -0
  50. pycapcut/exceptions.py +23 -0
  51. pycapcut/jianying_controller.py +230 -0
  52. pycapcut/keyframe.py +94 -0
  53. pycapcut/local_materials.py +198 -0
  54. pycapcut/metadata/__init__.py +51 -0
  55. pycapcut/metadata/audio_scene_effect.py +252 -0
  56. pycapcut/metadata/effect_meta.py +191 -0
  57. pycapcut/metadata/filter_meta.py +603 -0
  58. pycapcut/metadata/font_meta.py +341 -0
  59. pycapcut/metadata/mask_meta.py +15 -0
  60. pycapcut/metadata/speech_to_song.py +12 -0
  61. pycapcut/metadata/text_intro.py +152 -0
  62. pycapcut/metadata/text_loop.py +105 -0
  63. pycapcut/metadata/text_outro.py +105 -0
  64. pycapcut/metadata/tone_effect.py +107 -0
  65. pycapcut/metadata/transition_meta.py +733 -0
  66. pycapcut/metadata/video_character_effect.py +1913 -0
  67. pycapcut/metadata/video_group_animation.py +131 -0
  68. pycapcut/metadata/video_intro.py +103 -0
  69. pycapcut/metadata/video_outro.py +80 -0
  70. pycapcut/metadata/video_scene_effect.py +7706 -0
  71. pycapcut/script_file.py +808 -0
  72. pycapcut/segment.py +256 -0
  73. pycapcut/template_mode.py +221 -0
  74. pycapcut/text_segment.py +436 -0
  75. pycapcut/time_util.py +99 -0
  76. pycapcut/track.py +138 -0
  77. pycapcut/util.py +58 -0
  78. pycapcut/video_segment.py +526 -0
  79. pycapcut-0.0.1.dist-info/METADATA +77 -0
  80. pycapcut-0.0.1.dist-info/RECORD +88 -0
  81. pycapcut-0.0.1.dist-info/WHEEL +5 -0
  82. pycapcut-0.0.1.dist-info/top_level.txt +1 -0
  83. tools/__init__.py +17 -0
  84. tools/build_release.py +270 -0
  85. tools/code_generator.py +247 -0
  86. tools/extract_all.py +256 -0
  87. tools/extractor_base.py +132 -0
  88. 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
+ ]