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,225 @@
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, Any
11
+
12
+ from .time_util import tim, Timerange
13
+ from .segment import MediaSegment
14
+ from .local_materials import AudioMaterial
15
+ from .keyframe import KeyframeProperty, KeyframeList
16
+
17
+ from .metadata import EffectParamInstance
18
+ from .metadata import AudioSceneEffectType, ToneEffectType, SpeechToSongType
19
+
20
+ class AudioFade:
21
+ """音频淡入淡出效果"""
22
+
23
+ fade_id: str
24
+ """淡入淡出效果的全局id, 自动生成"""
25
+
26
+ in_duration: int
27
+ """淡入时长, 单位为微秒"""
28
+ out_duration: int
29
+ """淡出时长, 单位为微秒"""
30
+
31
+ def __init__(self, in_duration: int, out_duration: int):
32
+ """根据给定的淡入/淡出时长构造一个淡入淡出效果"""
33
+
34
+ self.fade_id = uuid.uuid4().hex
35
+ self.in_duration = in_duration
36
+ self.out_duration = out_duration
37
+
38
+ def export_json(self) -> Dict[str, Any]:
39
+ return {
40
+ "id": self.fade_id,
41
+ "fade_in_duration": self.in_duration,
42
+ "fade_out_duration": self.out_duration,
43
+ "fade_type": 0,
44
+ "type": "audio_fade"
45
+ }
46
+
47
+ class AudioEffect:
48
+ """音频特效对象"""
49
+
50
+ name: str
51
+ """特效名称"""
52
+ effect_id: str
53
+ """特效全局id, 由程序自动生成"""
54
+ resource_id: str
55
+ """资源id, 由剪映本身提供"""
56
+
57
+ category_id: Literal["sound_effect", "tone", "speech_to_song"]
58
+ category_name: Literal["场景音", "音色", "声音成曲"]
59
+ category_index: Literal[1, 2, 3]
60
+
61
+ audio_adjust_params: List[EffectParamInstance]
62
+
63
+ def __init__(self, effect_meta: Union[AudioSceneEffectType, ToneEffectType, SpeechToSongType],
64
+ params: Optional[List[Optional[float]]] = None):
65
+ """根据给定的音效元数据及参数列表构造一个音频特效对象, params的范围是0~100"""
66
+
67
+ self.name = effect_meta.value.name
68
+ self.effect_id = uuid.uuid4().hex
69
+ self.resource_id = effect_meta.value.resource_id
70
+ self.audio_adjust_params = []
71
+
72
+ if isinstance(effect_meta, AudioSceneEffectType):
73
+ self.category_id = "sound_effect"
74
+ self.category_name = "场景音"
75
+ self.category_index = 1
76
+ elif isinstance(effect_meta, ToneEffectType):
77
+ self.category_id = "tone"
78
+ self.category_name = "音色"
79
+ self.category_index = 2
80
+ elif isinstance(effect_meta, SpeechToSongType):
81
+ self.category_id = "speech_to_song"
82
+ self.category_name = "声音成曲"
83
+ self.category_index = 3
84
+ else:
85
+ raise TypeError("不支持的元数据类型 %s" % type(effect_meta))
86
+
87
+ self.audio_adjust_params = effect_meta.value.parse_params(params)
88
+
89
+ def export_json(self) -> Dict[str, Any]:
90
+ return {
91
+ "audio_adjust_params": [param.export_json() for param in self.audio_adjust_params],
92
+ "category_id": self.category_id,
93
+ "category_name": self.category_name,
94
+ "id": self.effect_id,
95
+ "is_ugc": False,
96
+ "name": self.name,
97
+ "production_path": "",
98
+ "resource_id": self.resource_id,
99
+ "speaker_id": "",
100
+ "sub_type": self.category_index,
101
+ "time_range": {"duration": 0, "start": 0}, # 似乎并未用到
102
+ "type": "audio_effect"
103
+ # 不导出path和constant_material_id
104
+ }
105
+
106
+ class AudioSegment(MediaSegment):
107
+ """安放在轨道上的一个音频片段"""
108
+
109
+ material_instance: AudioMaterial
110
+ """音频素材实例"""
111
+
112
+ fade: Optional[AudioFade]
113
+ """音频淡入淡出效果, 可能为空
114
+
115
+ 在放入轨道时自动添加到素材列表中
116
+ """
117
+
118
+ effects: List[AudioEffect]
119
+ """音频特效列表
120
+
121
+ 在放入轨道时自动添加到素材列表中
122
+ """
123
+
124
+ def __init__(self, material: Union[AudioMaterial, str], target_timerange: Timerange, *,
125
+ source_timerange: Optional[Timerange] = None, speed: Optional[float] = None, volume: float = 1.0):
126
+ """利用给定的音频素材构建一个轨道片段, 并指定其时间信息及播放速度/音量
127
+
128
+ Args:
129
+ material (`AudioMaterial` or `str`): 素材实例或素材路径, 若为路径则自动构造素材实例
130
+ target_timerange (`Timerange`): 片段在轨道上的目标时间范围
131
+ source_timerange (`Timerange`, optional): 截取的素材片段的时间范围, 默认从开头根据`speed`截取与`target_timerange`等长的一部分
132
+ speed (`float`, optional): 播放速度, 默认为1.0. 此项与`source_timerange`同时指定时, 将覆盖`target_timerange`中的时长
133
+ volume (`float`, optional): 音量, 默认为1.0
134
+
135
+ Raises:
136
+ `ValueError`: 指定的或计算出的`source_timerange`超出了素材的时长范围
137
+ """
138
+ if isinstance(material, str):
139
+ material = AudioMaterial(material)
140
+
141
+ if source_timerange is not None and speed is not None:
142
+ target_timerange = Timerange(target_timerange.start, round(source_timerange.duration / speed))
143
+ elif source_timerange is not None and speed is None:
144
+ speed = source_timerange.duration / target_timerange.duration
145
+ else: # source_timerange is None
146
+ speed = speed if speed is not None else 1.0
147
+ source_timerange = Timerange(0, round(target_timerange.duration * speed))
148
+
149
+ if source_timerange.end > material.duration:
150
+ raise ValueError(f"截取的素材时间范围 {source_timerange} 超出了素材时长({material.duration})")
151
+
152
+ super().__init__(material.material_id, source_timerange, target_timerange, speed, volume)
153
+
154
+ self.material_instance = deepcopy(material)
155
+ self.fade = None
156
+ self.effects = []
157
+
158
+ def add_effect(self, effect_type: Union[AudioSceneEffectType, ToneEffectType, SpeechToSongType],
159
+ params: Optional[List[Optional[float]]] = None) -> "AudioSegment":
160
+ """为音频片段添加一个作用于整个片段的音频效果, 目前"声音成曲"效果不能自动被剪映所识别
161
+
162
+ Args:
163
+ effect_type (`AudioSceneEffectType` | `ToneEffectType` | `SpeechToSongType`): 音效类型, 一类音效只能添加一个.
164
+ params (`List[Optional[float]]`, optional): 音效参数列表, 参数列表中未提供或为None的项使用默认值.
165
+ 参数取值范围(0~100)与剪映中一致. 某个特效类型有何参数以及具体参数顺序以枚举类成员的annotation为准.
166
+
167
+ Raises:
168
+ `ValueError`: 试图添加一个已经存在的音效类型、提供的参数数量超过了该音效类型的参数数量, 或参数值超出范围.
169
+ """
170
+ if params is not None and len(params) > len(effect_type.value.params):
171
+ raise ValueError("为音频效果 %s 传入了过多的参数" % effect_type.value.name)
172
+
173
+ effect_inst = AudioEffect(effect_type, params)
174
+ if effect_inst.category_id in [eff.category_id for eff in self.effects]:
175
+ raise ValueError("当前音频片段已经有此类型 (%s) 的音效了" % effect_inst.category_name)
176
+ self.effects.append(effect_inst)
177
+ self.extra_material_refs.append(effect_inst.effect_id)
178
+
179
+ return self
180
+
181
+ def add_fade(self, in_duration: Union[str, int], out_duration: Union[str, int]) -> "AudioSegment":
182
+ """为音频片段添加淡入淡出效果
183
+
184
+ Args:
185
+ in_duration (`int` or `str`): 音频淡入时长, 单位为微秒, 若为字符串则会调用`tim()`函数进行解析
186
+ out_duration (`int` or `str`): 音频淡出时长, 单位为微秒, 若为字符串则会调用`tim()`函数进行解析
187
+
188
+ Raises:
189
+ `ValueError`: 当前片段已存在淡入淡出效果
190
+ """
191
+ if self.fade is not None:
192
+ raise ValueError("当前片段已存在淡入淡出效果")
193
+
194
+ if isinstance(in_duration, str): in_duration = tim(in_duration)
195
+ if isinstance(out_duration, str): out_duration = tim(out_duration)
196
+
197
+ self.fade = AudioFade(in_duration, out_duration)
198
+ self.extra_material_refs.append(self.fade.fade_id)
199
+
200
+ return self
201
+
202
+ def add_keyframe(self, time_offset: int, volume: float) -> "AudioSegment":
203
+ """为音频片段创建一个*控制音量*的关键帧, 并自动加入到关键帧列表中
204
+
205
+ Args:
206
+ time_offset (`int`): 关键帧的时间偏移量, 单位为微秒
207
+ volume (`float`): 音量在`time_offset`处的值
208
+ """
209
+ _property = KeyframeProperty.volume
210
+ for kf_list in self.common_keyframes:
211
+ if kf_list.keyframe_property == _property:
212
+ kf_list.add_keyframe(time_offset, volume)
213
+ return self
214
+ kf_list = KeyframeList(_property)
215
+ kf_list.add_keyframe(time_offset, volume)
216
+ self.common_keyframes.append(kf_list)
217
+ return self
218
+
219
+ def export_json(self) -> Dict[str, Any]:
220
+ json_dict = super().export_json()
221
+ json_dict.update({
222
+ "clip": None,
223
+ "hdr_settings": None
224
+ })
225
+ return json_dict
@@ -0,0 +1,119 @@
1
+ {
2
+ "canvas_config": {
3
+ "height": 1080,
4
+ "ratio": "original",
5
+ "width": 1920
6
+ },
7
+ "color_space": 0,
8
+ "config": {
9
+ "adjust_max_index": 1,
10
+ "attachment_info": [],
11
+ "combination_max_index": 1,
12
+ "export_range": null,
13
+ "extract_audio_last_index": 1,
14
+ "lyrics_recognition_id": "",
15
+ "lyrics_sync": true,
16
+ "lyrics_taskinfo": [],
17
+ "maintrack_adsorb": true,
18
+ "material_save_mode": 0,
19
+ "multi_language_current": "none",
20
+ "multi_language_list": [],
21
+ "multi_language_main": "none",
22
+ "multi_language_mode": "none",
23
+ "original_sound_last_index": 1,
24
+ "record_audio_last_index": 1,
25
+ "sticker_max_index": 1,
26
+ "subtitle_keywords_config": null,
27
+ "subtitle_recognition_id": "",
28
+ "subtitle_sync": true,
29
+ "subtitle_taskinfo": [],
30
+ "system_font_list": [],
31
+ "video_mute": false,
32
+ "zoom_info_params": null
33
+ },
34
+ "cover": null,
35
+ "create_time": 0,
36
+ "duration": 0,
37
+ "extra_info": null,
38
+ "fps": 30.0,
39
+ "free_render_index_mode_on": false,
40
+ "group_container": null,
41
+ "id": "91E08AC5-22FB-47e2-9AA0-7DC300FAEA2B",
42
+ "keyframe_graph_list": [],
43
+ "keyframes": {
44
+ "adjusts": [],
45
+ "audios": [],
46
+ "effects": [],
47
+ "filters": [],
48
+ "handwrites": [],
49
+ "stickers": [],
50
+ "texts": [],
51
+ "videos": []
52
+ },
53
+ "last_modified_platform": {
54
+ "app_id": 3704,
55
+ "app_source": "lv",
56
+ "app_version": "5.9.0",
57
+ "os": "windows"
58
+ },
59
+ "materials": {
60
+ "ai_translates": [],
61
+ "audio_balances": [],
62
+ "audio_effects": [],
63
+ "audio_fades": [],
64
+ "audio_track_indexes": [],
65
+ "audios": [],
66
+ "beats": [],
67
+ "canvases": [],
68
+ "chromas": [],
69
+ "color_curves": [],
70
+ "digital_humans": [],
71
+ "drafts": [],
72
+ "effects": [],
73
+ "flowers": [],
74
+ "green_screens": [],
75
+ "handwrites": [],
76
+ "hsl": [],
77
+ "images": [],
78
+ "log_color_wheels": [],
79
+ "loudnesses": [],
80
+ "manual_deformations": [],
81
+ "masks": [],
82
+ "material_animations": [],
83
+ "material_colors": [],
84
+ "multi_language_refs": [],
85
+ "placeholders": [],
86
+ "plugin_effects": [],
87
+ "primary_color_wheels": [],
88
+ "realtime_denoises": [],
89
+ "shapes": [],
90
+ "smart_crops": [],
91
+ "smart_relights": [],
92
+ "sound_channel_mappings": [],
93
+ "speeds": [],
94
+ "stickers": [],
95
+ "tail_leaders": [],
96
+ "text_templates": [],
97
+ "texts": [],
98
+ "time_marks": [],
99
+ "transitions": [],
100
+ "video_effects": [],
101
+ "video_trackings": [],
102
+ "videos": [],
103
+ "vocal_beautifys": [],
104
+ "vocal_separations": []
105
+ },
106
+ "mutable_config": null,
107
+ "name": "",
108
+ "new_version": "110.0.0",
109
+ "relationships": [],
110
+ "render_index_track_mode_on": false,
111
+ "retouch_cover": null,
112
+ "source": "default",
113
+ "static_cover_image_path": "",
114
+ "time_marks": null,
115
+ "tracks": [
116
+ ],
117
+ "update_time": 0,
118
+ "version": 360000
119
+ }
@@ -0,0 +1,153 @@
1
+ """草稿文件夹管理器"""
2
+
3
+ import os
4
+ import shutil
5
+
6
+ from typing import List
7
+
8
+ from . import assets
9
+ from .script_file import ScriptFile
10
+
11
+ class DraftFolder:
12
+ """管理一个文件夹及其内的一系列草稿"""
13
+
14
+ folder_path: str
15
+ """根路径"""
16
+
17
+ def __init__(self, folder_path: str):
18
+ """初始化草稿文件夹管理器
19
+
20
+ Args:
21
+ folder_path (`str`): 包含若干草稿的文件夹, 一般取剪映保存草稿的位置即可
22
+
23
+ Raises:
24
+ `FileNotFoundError`: 路径不存在
25
+ """
26
+ self.folder_path = folder_path
27
+
28
+ if not os.path.exists(self.folder_path):
29
+ raise FileNotFoundError(f"根文件夹 {self.folder_path} 不存在")
30
+
31
+ def list_drafts(self) -> List[str]:
32
+ """列出文件夹中所有草稿的名称
33
+
34
+ 注意: 本函数只是如实地列出子文件夹的名称, 并不检查它们是否符合草稿的格式
35
+ """
36
+ return [f for f in os.listdir(self.folder_path) if os.path.isdir(os.path.join(self.folder_path, f))]
37
+
38
+ def has_draft(self, draft_name: str) -> bool:
39
+ """检查文件夹中是否存在指定名称的草稿
40
+
41
+ 注意: 本函数只检查文件夹是否存在, 并不检查草稿是否符合剪映的格式
42
+
43
+ Args:
44
+ draft_name (`str`): 草稿名称, 即相应文件夹名称
45
+ """
46
+ return draft_name in self.list_drafts()
47
+
48
+ def remove(self, draft_name: str) -> None:
49
+ """删除指定名称的草稿
50
+
51
+ Args:
52
+ draft_name (`str`): 草稿名称, 即相应文件夹名称
53
+
54
+ Raises:
55
+ `FileNotFoundError`: 对应的草稿不存在
56
+ """
57
+ draft_path = os.path.join(self.folder_path, draft_name)
58
+ if not os.path.exists(draft_path):
59
+ raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在")
60
+
61
+ shutil.rmtree(draft_path)
62
+
63
+ def create_draft(self, draft_name: str, width: int, height: int, fps: int = 30, *,
64
+ allow_replace: bool = False) -> ScriptFile:
65
+ """创建一个新草稿并开始编辑, 编辑完成后使用`ScriptFile.save()`保存即可
66
+
67
+ Args:
68
+ draft_name (`str`): 草稿名称, 即相应文件夹名称
69
+ width (`int`): 视频宽度, 单位为像素
70
+ height (`int`): 视频高度, 单位为像素
71
+ fps (`int`, optional): 视频帧率. 默认为30.
72
+ allow_replace (`bool`, optional): 是否允许覆盖与`draft_name`重名的草稿. 默认为否.
73
+
74
+ Raises:
75
+ `FileExistsError`: 已存在与`draft_name`重名的草稿, 但不允许覆盖.
76
+ """
77
+ draft_path = os.path.join(self.folder_path, draft_name)
78
+ if os.path.exists(draft_path):
79
+ if not allow_replace:
80
+ raise FileExistsError(f"草稿文件夹 {draft_name} 已存在且不允许覆盖")
81
+ shutil.rmtree(draft_path)
82
+
83
+ # 创建草稿文件夹
84
+ os.makedirs(draft_path)
85
+ shutil.copy(assets.get_asset_path("DRAFT_META_TEMPLATE"), os.path.join(draft_path, "draft_meta_info.json"))
86
+
87
+ # 创建草稿文件
88
+ script_file = ScriptFile(width, height, fps)
89
+ script_file.save_path = os.path.join(draft_path, "draft_content.json")
90
+
91
+ return script_file
92
+
93
+ def inspect_material(self, draft_name: str) -> None:
94
+ """输出指定名称草稿中的贴纸素材元数据
95
+
96
+ Args:
97
+ draft_name (`str`): 草稿名称, 即相应文件夹名称
98
+
99
+ Raises:
100
+ `FileNotFoundError`: 对应的草稿不存在
101
+ """
102
+ draft_path = os.path.join(self.folder_path, draft_name)
103
+ if not os.path.exists(draft_path):
104
+ raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在")
105
+
106
+ script_file = self.load_template(draft_name)
107
+ script_file.inspect_material()
108
+
109
+ def load_template(self, draft_name: str) -> ScriptFile:
110
+ """在文件夹中打开一个草稿作为模板, 并在其上进行编辑
111
+
112
+ Args:
113
+ draft_name (`str`): 草稿名称, 即相应文件夹名称
114
+
115
+ Returns:
116
+ `ScriptFile`: 以模板模式打开的草稿对象
117
+
118
+ Raises:
119
+ `FileNotFoundError`: 对应的草稿不存在
120
+ """
121
+ draft_path = os.path.join(self.folder_path, draft_name)
122
+ if not os.path.exists(draft_path):
123
+ raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在")
124
+
125
+ return ScriptFile.load_template(os.path.join(draft_path, "draft_content.json"))
126
+
127
+ def duplicate_as_template(self, template_name: str, new_draft_name: str, allow_replace: bool = False) -> ScriptFile:
128
+ """复制一份给定的草稿, 并在复制出的新草稿上进行编辑
129
+
130
+ Args:
131
+ template_name (`str`): 原草稿名称
132
+ new_draft_name (`str`): 新草稿名称
133
+ allow_replace (`bool`, optional): 是否允许覆盖与`new_draft_name`重名的草稿. 默认为否.
134
+
135
+ Returns:
136
+ `ScriptFile`: 以模板模式打开的**复制后的**草稿对象
137
+
138
+ Raises:
139
+ `FileNotFoundError`: 原始草稿不存在
140
+ `FileExistsError`: 已存在与`new_draft_name`重名的草稿, 但不允许覆盖.
141
+ """
142
+ template_path = os.path.join(self.folder_path, template_name)
143
+ new_draft_path = os.path.join(self.folder_path, new_draft_name)
144
+ if not os.path.exists(template_path):
145
+ raise FileNotFoundError(f"模板草稿 {template_name} 不存在")
146
+ if os.path.exists(new_draft_path) and not allow_replace:
147
+ raise FileExistsError(f"新草稿 {new_draft_name} 已存在且不允许覆盖")
148
+
149
+ # 复制草稿文件夹
150
+ shutil.copytree(template_path, new_draft_path, dirs_exist_ok=allow_replace)
151
+
152
+ # 打开草稿
153
+ return self.load_template(new_draft_name)
@@ -0,0 +1,36 @@
1
+ """定义特效/滤镜片段类"""
2
+
3
+ from typing import Union, Optional, List
4
+
5
+ from .time_util import Timerange
6
+ from .segment import BaseSegment
7
+ from .video_segment import VideoEffect, Filter
8
+
9
+ from .metadata import VideoSceneEffectType, VideoCharacterEffectType, FilterType
10
+
11
+ class EffectSegment(BaseSegment):
12
+ """放置在独立特效轨道上的特效片段"""
13
+
14
+ effect_inst: VideoEffect
15
+ """相应的特效素材
16
+
17
+ 在放入轨道时自动添加到素材列表中
18
+ """
19
+
20
+ def __init__(self, effect_type: Union[VideoSceneEffectType, VideoCharacterEffectType],
21
+ target_timerange: Timerange, params: Optional[List[Optional[float]]] = None):
22
+ self.effect_inst = VideoEffect(effect_type, params, apply_target_type=2) # 作用域为全局
23
+ super().__init__(self.effect_inst.global_id, target_timerange)
24
+
25
+ class FilterSegment(BaseSegment):
26
+ """放置在独立滤镜轨道上的滤镜片段"""
27
+
28
+ material: Filter
29
+ """相应的滤镜素材
30
+
31
+ 在放入轨道时自动添加到素材列表中
32
+ """
33
+
34
+ def __init__(self, meta: FilterType, target_timerange: Timerange, intensity: float):
35
+ self.material = Filter(meta.value, intensity)
36
+ super().__init__(self.material.global_id, target_timerange)
@@ -0,0 +1,23 @@
1
+ """自定义异常类"""
2
+
3
+ class TrackNotFound(NameError):
4
+ """未找到满足条件的轨道"""
5
+ class AmbiguousTrack(ValueError):
6
+ """找到多个满足条件的轨道"""
7
+ class SegmentOverlap(ValueError):
8
+ """新片段与已有的轨道片段重叠"""
9
+
10
+ class MaterialNotFound(NameError):
11
+ """未找到满足条件的素材"""
12
+ class AmbiguousMaterial(ValueError):
13
+ """找到多个满足条件的素材"""
14
+
15
+ class ExtensionFailed(ValueError):
16
+ """替换素材时延伸片段失败"""
17
+
18
+ class DraftNotFound(NameError):
19
+ """未找到草稿"""
20
+ class AutomationError(Exception):
21
+ """自动化操作失败"""
22
+ class ExportTimeout(Exception):
23
+ """导出超时"""