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,256 @@
|
|
|
1
|
+
"""定义片段基类及部分比较通用的属性类"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional, Dict, List, Any, Union
|
|
5
|
+
|
|
6
|
+
from .animation import SegmentAnimations
|
|
7
|
+
from .time_util import Timerange, tim
|
|
8
|
+
from .keyframe import KeyframeList, KeyframeProperty
|
|
9
|
+
|
|
10
|
+
class BaseSegment:
|
|
11
|
+
"""片段基类"""
|
|
12
|
+
|
|
13
|
+
segment_id: str
|
|
14
|
+
"""片段全局id, 由程序自动生成"""
|
|
15
|
+
material_id: str
|
|
16
|
+
"""使用的素材id"""
|
|
17
|
+
target_timerange: Timerange
|
|
18
|
+
"""片段在轨道上的时间范围"""
|
|
19
|
+
|
|
20
|
+
common_keyframes: List[KeyframeList]
|
|
21
|
+
"""各属性的关键帧列表"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, material_id: str, target_timerange: Timerange):
|
|
24
|
+
self.segment_id = uuid.uuid4().hex
|
|
25
|
+
self.material_id = material_id
|
|
26
|
+
self.target_timerange = target_timerange
|
|
27
|
+
|
|
28
|
+
self.common_keyframes = []
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def start(self) -> int:
|
|
32
|
+
"""片段开始时间, 单位为微秒"""
|
|
33
|
+
return self.target_timerange.start
|
|
34
|
+
@start.setter
|
|
35
|
+
def start(self, value: int):
|
|
36
|
+
self.target_timerange.start = value
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def duration(self) -> int:
|
|
40
|
+
"""片段持续时间, 单位为微秒"""
|
|
41
|
+
return self.target_timerange.duration
|
|
42
|
+
@duration.setter
|
|
43
|
+
def duration(self, value: int):
|
|
44
|
+
self.target_timerange.duration = value
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def end(self) -> int:
|
|
48
|
+
"""片段结束时间, 单位为微秒"""
|
|
49
|
+
return self.target_timerange.end
|
|
50
|
+
|
|
51
|
+
def overlaps(self, other: "BaseSegment") -> bool:
|
|
52
|
+
"""判断是否与另一个片段有重叠"""
|
|
53
|
+
return self.target_timerange.overlaps(other.target_timerange)
|
|
54
|
+
|
|
55
|
+
def export_json(self) -> Dict[str, Any]:
|
|
56
|
+
"""返回通用于各种片段的属性"""
|
|
57
|
+
return {
|
|
58
|
+
"enable_adjust": True,
|
|
59
|
+
"enable_color_correct_adjust": False,
|
|
60
|
+
"enable_color_curves": True,
|
|
61
|
+
"enable_color_match_adjust": False,
|
|
62
|
+
"enable_color_wheels": True,
|
|
63
|
+
"enable_lut": True,
|
|
64
|
+
"enable_smart_color_adjust": False,
|
|
65
|
+
"last_nonzero_volume": 1.0,
|
|
66
|
+
"reverse": False,
|
|
67
|
+
"track_attribute": 0,
|
|
68
|
+
"track_render_index": 0,
|
|
69
|
+
"visible": True,
|
|
70
|
+
# 写入自定义字段
|
|
71
|
+
"id": self.segment_id,
|
|
72
|
+
"material_id": self.material_id,
|
|
73
|
+
"target_timerange": self.target_timerange.export_json(),
|
|
74
|
+
|
|
75
|
+
"common_keyframes": [kf_list.export_json() for kf_list in self.common_keyframes],
|
|
76
|
+
"keyframe_refs": [], # 意义不明
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class Speed:
|
|
80
|
+
"""播放速度对象, 目前只支持固定速度"""
|
|
81
|
+
|
|
82
|
+
global_id: str
|
|
83
|
+
"""全局id, 由程序自动生成"""
|
|
84
|
+
speed: float
|
|
85
|
+
"""播放速度"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, speed: float):
|
|
88
|
+
self.global_id = uuid.uuid4().hex
|
|
89
|
+
self.speed = speed
|
|
90
|
+
|
|
91
|
+
def export_json(self) -> Dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"curve_speed": None,
|
|
94
|
+
"id": self.global_id,
|
|
95
|
+
"mode": 0,
|
|
96
|
+
"speed": self.speed,
|
|
97
|
+
"type": "speed"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class ClipSettings:
|
|
101
|
+
"""素材片段的图像调节设置"""
|
|
102
|
+
|
|
103
|
+
alpha: float
|
|
104
|
+
"""图像不透明度, 0-1"""
|
|
105
|
+
flip_horizontal: bool
|
|
106
|
+
"""是否水平翻转"""
|
|
107
|
+
flip_vertical: bool
|
|
108
|
+
"""是否垂直翻转"""
|
|
109
|
+
rotation: float
|
|
110
|
+
"""顺时针旋转的**角度**, 可正可负"""
|
|
111
|
+
scale_x: float
|
|
112
|
+
"""水平缩放比例"""
|
|
113
|
+
scale_y: float
|
|
114
|
+
"""垂直缩放比例"""
|
|
115
|
+
transform_x: float
|
|
116
|
+
"""水平位移, 单位为半个画布宽"""
|
|
117
|
+
transform_y: float
|
|
118
|
+
"""垂直位移, 单位为半个画布高"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, *, alpha: float = 1.0,
|
|
121
|
+
flip_horizontal: bool = False, flip_vertical: bool = False,
|
|
122
|
+
rotation: float = 0.0,
|
|
123
|
+
scale_x: float = 1.0, scale_y: float = 1.0,
|
|
124
|
+
transform_x: float = 0.0, transform_y: float = 0.0):
|
|
125
|
+
"""初始化图像调节设置, 默认不作任何图像变换
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
alpha (float, optional): 图像不透明度, 0-1. 默认为1.0.
|
|
129
|
+
flip_horizontal (bool, optional): 是否水平翻转. 默认为False.
|
|
130
|
+
flip_vertical (bool, optional): 是否垂直翻转. 默认为False.
|
|
131
|
+
rotation (float, optional): 顺时针旋转的**角度**, 可正可负. 默认为0.0.
|
|
132
|
+
scale_x (float, optional): 水平缩放比例. 默认为1.0.
|
|
133
|
+
scale_y (float, optional): 垂直缩放比例. 默认为1.0.
|
|
134
|
+
transform_x (float, optional): 水平位移, 单位为半个画布宽. 默认为0.0.
|
|
135
|
+
transform_y (float, optional): 垂直位移, 单位为半个画布高. 默认为0.0.
|
|
136
|
+
参考: 剪映导入的字幕似乎取此值为-0.8
|
|
137
|
+
"""
|
|
138
|
+
self.alpha = alpha
|
|
139
|
+
self.flip_horizontal, self.flip_vertical = flip_horizontal, flip_vertical
|
|
140
|
+
self.rotation = rotation
|
|
141
|
+
self.scale_x, self.scale_y = scale_x, scale_y
|
|
142
|
+
self.transform_x, self.transform_y = transform_x, transform_y
|
|
143
|
+
|
|
144
|
+
def export_json(self) -> Dict[str, Any]:
|
|
145
|
+
clip_settings_json = {
|
|
146
|
+
"alpha": self.alpha,
|
|
147
|
+
"flip": {"horizontal": self.flip_horizontal, "vertical": self.flip_vertical},
|
|
148
|
+
"rotation": self.rotation,
|
|
149
|
+
"scale": {"x": self.scale_x, "y": self.scale_y},
|
|
150
|
+
"transform": {"x": self.transform_x, "y": self.transform_y}
|
|
151
|
+
}
|
|
152
|
+
return clip_settings_json
|
|
153
|
+
|
|
154
|
+
class MediaSegment(BaseSegment):
|
|
155
|
+
"""媒体片段基类"""
|
|
156
|
+
|
|
157
|
+
source_timerange: Optional[Timerange]
|
|
158
|
+
"""截取的素材片段的时间范围, 对贴纸而言不存在"""
|
|
159
|
+
speed: Speed
|
|
160
|
+
"""播放速度设置"""
|
|
161
|
+
volume: float
|
|
162
|
+
"""音量"""
|
|
163
|
+
|
|
164
|
+
extra_material_refs: List[str]
|
|
165
|
+
"""附加的素材id列表, 用于链接动画/特效等"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, material_id: str, source_timerange: Optional[Timerange], target_timerange: Timerange, speed: float, volume: float):
|
|
168
|
+
super().__init__(material_id, target_timerange)
|
|
169
|
+
|
|
170
|
+
self.source_timerange = source_timerange
|
|
171
|
+
self.speed = Speed(speed)
|
|
172
|
+
self.volume = volume
|
|
173
|
+
|
|
174
|
+
self.extra_material_refs = [self.speed.global_id]
|
|
175
|
+
|
|
176
|
+
def export_json(self) -> Dict[str, Any]:
|
|
177
|
+
"""返回通用于音频和视频片段的默认属性"""
|
|
178
|
+
ret = super().export_json()
|
|
179
|
+
ret.update({
|
|
180
|
+
"source_timerange": self.source_timerange.export_json() if self.source_timerange else None,
|
|
181
|
+
"speed": self.speed.speed,
|
|
182
|
+
"volume": self.volume,
|
|
183
|
+
"extra_material_refs": self.extra_material_refs,
|
|
184
|
+
})
|
|
185
|
+
return ret
|
|
186
|
+
|
|
187
|
+
class VisualSegment(MediaSegment):
|
|
188
|
+
"""视觉片段基类,用于处理所有可见片段(视频、贴纸、文本)的共同属性和行为"""
|
|
189
|
+
|
|
190
|
+
clip_settings: ClipSettings
|
|
191
|
+
"""图像调节设置, 其效果可被关键帧覆盖"""
|
|
192
|
+
|
|
193
|
+
uniform_scale: bool
|
|
194
|
+
"""是否锁定XY轴缩放比例"""
|
|
195
|
+
|
|
196
|
+
animations_instance: Optional[SegmentAnimations]
|
|
197
|
+
"""动画实例, 可能为空
|
|
198
|
+
|
|
199
|
+
在放入轨道时自动添加到素材列表中
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, material_id: str, source_timerange: Optional[Timerange], target_timerange: Timerange,
|
|
203
|
+
speed: float, volume: float, *, clip_settings: Optional[ClipSettings]):
|
|
204
|
+
"""初始化视觉片段基类
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
material_id (`str`): 素材id
|
|
208
|
+
source_timerange (`Timerange`, optional): 截取的素材片段的时间范围
|
|
209
|
+
target_timerange (`Timerange`): 片段在轨道上的目标时间范围
|
|
210
|
+
speed (`float`): 播放速度
|
|
211
|
+
volume (`float`): 音量
|
|
212
|
+
clip_settings (`ClipSettings`, optional): 图像调节设置, 默认不作任何变换
|
|
213
|
+
"""
|
|
214
|
+
super().__init__(material_id, source_timerange, target_timerange, speed, volume)
|
|
215
|
+
|
|
216
|
+
self.clip_settings = clip_settings if clip_settings is not None else ClipSettings()
|
|
217
|
+
self.uniform_scale = True
|
|
218
|
+
self.animations_instance = None
|
|
219
|
+
|
|
220
|
+
def add_keyframe(self, _property: KeyframeProperty, time_offset: Union[int, str], value: float) -> "VisualSegment":
|
|
221
|
+
"""为给定属性创建一个关键帧, 并自动加入到关键帧列表中
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
_property (`KeyframeProperty`): 要控制的属性
|
|
225
|
+
time_offset (`int` or `str`): 关键帧的时间偏移量, 单位为微秒. 若传入字符串则会调用`tim()`函数进行解析.
|
|
226
|
+
value (`float`): 属性在`time_offset`处的值
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
`ValueError`: 试图同时设置`uniform_scale`以及`scale_x`或`scale_y`其中一者
|
|
230
|
+
"""
|
|
231
|
+
if (_property == KeyframeProperty.scale_x or _property == KeyframeProperty.scale_y) and self.uniform_scale:
|
|
232
|
+
self.uniform_scale = False
|
|
233
|
+
elif _property == KeyframeProperty.uniform_scale:
|
|
234
|
+
if not self.uniform_scale:
|
|
235
|
+
raise ValueError("已设置 scale_x 或 scale_y 时, 不能再设置 uniform_scale")
|
|
236
|
+
_property = KeyframeProperty.scale_x
|
|
237
|
+
|
|
238
|
+
if isinstance(time_offset, str): time_offset = tim(time_offset)
|
|
239
|
+
|
|
240
|
+
for kf_list in self.common_keyframes:
|
|
241
|
+
if kf_list.keyframe_property == _property:
|
|
242
|
+
kf_list.add_keyframe(time_offset, value)
|
|
243
|
+
return self
|
|
244
|
+
kf_list = KeyframeList(_property)
|
|
245
|
+
kf_list.add_keyframe(time_offset, value)
|
|
246
|
+
self.common_keyframes.append(kf_list)
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def export_json(self) -> Dict[str, Any]:
|
|
250
|
+
"""导出通用于所有视觉片段的JSON数据"""
|
|
251
|
+
json_dict = super().export_json()
|
|
252
|
+
json_dict.update({
|
|
253
|
+
"clip": self.clip_settings.export_json(),
|
|
254
|
+
"uniform_scale": {"on": self.uniform_scale, "value": 1.0},
|
|
255
|
+
})
|
|
256
|
+
return json_dict
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""与模板模式相关的类及函数等"""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
|
|
6
|
+
from . import util
|
|
7
|
+
from . import exceptions
|
|
8
|
+
from .time_util import Timerange
|
|
9
|
+
from .segment import BaseSegment
|
|
10
|
+
from .track import BaseTrack, TrackType
|
|
11
|
+
from .local_materials import VideoMaterial, AudioMaterial
|
|
12
|
+
|
|
13
|
+
from typing import List, Dict, Any
|
|
14
|
+
|
|
15
|
+
class ShrinkMode(Enum):
|
|
16
|
+
"""处理替换素材时素材变短情况的方法"""
|
|
17
|
+
|
|
18
|
+
cut_head = "cut_head"
|
|
19
|
+
"""裁剪头部, 即后移片段起始点"""
|
|
20
|
+
cut_tail = "cut_tail"
|
|
21
|
+
"""裁剪尾部, 即前移片段终止点"""
|
|
22
|
+
|
|
23
|
+
cut_tail_align = "cut_tail_align"
|
|
24
|
+
"""裁剪尾部并消除间隙, 即前移片段终止点, 后续片段也依次前移"""
|
|
25
|
+
|
|
26
|
+
shrink = "shrink"
|
|
27
|
+
"""保持中间点不变, 两端点向中间靠拢"""
|
|
28
|
+
|
|
29
|
+
class ExtendMode(Enum):
|
|
30
|
+
"""处理替换素材时素材变长情况的方法"""
|
|
31
|
+
|
|
32
|
+
cut_material_tail = "cut_material_tail"
|
|
33
|
+
"""裁剪素材尾部(覆盖`source_timerange`参数), 使得片段维持原长不变, 此方法总是成功"""
|
|
34
|
+
|
|
35
|
+
extend_head = "extend_head"
|
|
36
|
+
"""延伸头部, 即尝试前移片段起始点, 与前续片段重合时失败"""
|
|
37
|
+
extend_tail = "extend_tail"
|
|
38
|
+
"""延伸尾部, 即尝试后移片段终止点, 与后续片段重合时失败"""
|
|
39
|
+
|
|
40
|
+
push_tail = "push_tail"
|
|
41
|
+
"""延伸尾部, 若有必要则依次后移后续片段, 此方法总是成功"""
|
|
42
|
+
|
|
43
|
+
class ImportedSegment(BaseSegment):
|
|
44
|
+
"""导入的片段"""
|
|
45
|
+
|
|
46
|
+
raw_data: Dict[str, Any]
|
|
47
|
+
"""原始json数据"""
|
|
48
|
+
|
|
49
|
+
__DATA_ATTRS = ["material_id", "target_timerange"]
|
|
50
|
+
def __init__(self, json_data: Dict[str, Any]):
|
|
51
|
+
self.raw_data = deepcopy(json_data)
|
|
52
|
+
|
|
53
|
+
util.assign_attr_with_json(self, self.__DATA_ATTRS, json_data)
|
|
54
|
+
|
|
55
|
+
def export_json(self) -> Dict[str, Any]:
|
|
56
|
+
json_data = deepcopy(self.raw_data)
|
|
57
|
+
json_data.update(util.export_attr_to_json(self, self.__DATA_ATTRS))
|
|
58
|
+
return json_data
|
|
59
|
+
|
|
60
|
+
class ImportedMediaSegment(ImportedSegment):
|
|
61
|
+
"""导入的视频/音频片段"""
|
|
62
|
+
|
|
63
|
+
source_timerange: Timerange
|
|
64
|
+
"""片段取用的素材时间范围"""
|
|
65
|
+
|
|
66
|
+
__DATA_ATTRS = ["source_timerange"]
|
|
67
|
+
def __init__(self, json_data: Dict[str, Any]):
|
|
68
|
+
super().__init__(json_data)
|
|
69
|
+
|
|
70
|
+
util.assign_attr_with_json(self, self.__DATA_ATTRS, json_data)
|
|
71
|
+
|
|
72
|
+
def export_json(self) -> Dict[str, Any]:
|
|
73
|
+
json_data = super().export_json()
|
|
74
|
+
json_data.update(util.export_attr_to_json(self, self.__DATA_ATTRS))
|
|
75
|
+
return json_data
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ImportedTrack(BaseTrack):
|
|
79
|
+
"""模板模式下导入的轨道"""
|
|
80
|
+
|
|
81
|
+
raw_data: Dict[str, Any]
|
|
82
|
+
"""原始轨道数据"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, json_data: Dict[str, Any]):
|
|
85
|
+
self.track_type = TrackType.from_name(json_data["type"])
|
|
86
|
+
self.name = json_data["name"]
|
|
87
|
+
self.track_id = json_data["id"]
|
|
88
|
+
self.render_index = max([int(seg["render_index"]) for seg in json_data["segments"]], default=0)
|
|
89
|
+
|
|
90
|
+
self.raw_data = deepcopy(json_data)
|
|
91
|
+
|
|
92
|
+
def export_json(self) -> Dict[str, Any]:
|
|
93
|
+
ret = deepcopy(self.raw_data)
|
|
94
|
+
ret.update({
|
|
95
|
+
"name": self.name,
|
|
96
|
+
"id": self.track_id
|
|
97
|
+
})
|
|
98
|
+
return ret
|
|
99
|
+
|
|
100
|
+
class EditableTrack(ImportedTrack):
|
|
101
|
+
"""模板模式下导入且可修改的轨道(音视频及文本轨道)"""
|
|
102
|
+
|
|
103
|
+
segments: List[ImportedSegment]
|
|
104
|
+
"""该轨道包含的片段列表"""
|
|
105
|
+
|
|
106
|
+
def __len__(self):
|
|
107
|
+
return len(self.segments)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def start_time(self) -> int:
|
|
111
|
+
"""轨道起始时间, 微秒"""
|
|
112
|
+
if len(self.segments) == 0:
|
|
113
|
+
return 0
|
|
114
|
+
return self.segments[0].target_timerange.start
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def end_time(self) -> int:
|
|
118
|
+
"""轨道结束时间, 微秒"""
|
|
119
|
+
if len(self.segments) == 0:
|
|
120
|
+
return 0
|
|
121
|
+
return self.segments[-1].target_timerange.end
|
|
122
|
+
|
|
123
|
+
def export_json(self) -> Dict[str, Any]:
|
|
124
|
+
ret = super().export_json()
|
|
125
|
+
# 为每个片段写入render_index
|
|
126
|
+
segment_exports = [seg.export_json() for seg in self.segments]
|
|
127
|
+
for seg in segment_exports:
|
|
128
|
+
seg["render_index"] = self.render_index
|
|
129
|
+
ret["segments"] = segment_exports
|
|
130
|
+
return ret
|
|
131
|
+
|
|
132
|
+
class ImportedTextTrack(EditableTrack):
|
|
133
|
+
"""模板模式下导入的文本轨道"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, json_data: Dict[str, Any]):
|
|
136
|
+
super().__init__(json_data)
|
|
137
|
+
self.segments = [ImportedSegment(seg) for seg in json_data["segments"]]
|
|
138
|
+
|
|
139
|
+
class ImportedMediaTrack(EditableTrack):
|
|
140
|
+
"""模板模式下导入的音频/视频轨道"""
|
|
141
|
+
|
|
142
|
+
segments: List[ImportedMediaSegment]
|
|
143
|
+
"""该轨道包含的片段列表"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, json_data: Dict[str, Any]):
|
|
146
|
+
super().__init__(json_data)
|
|
147
|
+
self.segments = [ImportedMediaSegment(seg) for seg in json_data["segments"]]
|
|
148
|
+
|
|
149
|
+
def check_material_type(self, material: object) -> bool:
|
|
150
|
+
"""检查素材类型是否与轨道类型匹配"""
|
|
151
|
+
if self.track_type == TrackType.video and isinstance(material, VideoMaterial):
|
|
152
|
+
return True
|
|
153
|
+
if self.track_type == TrackType.audio and isinstance(material, AudioMaterial):
|
|
154
|
+
return True
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def process_timerange(self, seg_index: int, src_timerange: Timerange,
|
|
158
|
+
shrink: ShrinkMode, extend: List[ExtendMode]) -> None:
|
|
159
|
+
"""处理素材替换的时间范围变更"""
|
|
160
|
+
seg = self.segments[seg_index]
|
|
161
|
+
new_duration = src_timerange.duration
|
|
162
|
+
|
|
163
|
+
# 时长变短
|
|
164
|
+
delta_duration = abs(new_duration - seg.duration)
|
|
165
|
+
if new_duration < seg.duration:
|
|
166
|
+
if shrink == ShrinkMode.cut_head:
|
|
167
|
+
seg.start += delta_duration
|
|
168
|
+
elif shrink == ShrinkMode.cut_tail:
|
|
169
|
+
seg.duration -= delta_duration
|
|
170
|
+
elif shrink == ShrinkMode.cut_tail_align:
|
|
171
|
+
seg.duration -= delta_duration
|
|
172
|
+
for i in range(seg_index+1, len(self.segments)): # 后续片段也依次前移相应值(保持间隙)
|
|
173
|
+
self.segments[i].start -= delta_duration
|
|
174
|
+
elif shrink == ShrinkMode.shrink:
|
|
175
|
+
seg.duration -= delta_duration
|
|
176
|
+
seg.start += delta_duration // 2
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError(f"Unsupported shrink mode: {shrink}")
|
|
179
|
+
# 时长变长
|
|
180
|
+
elif new_duration > seg.duration:
|
|
181
|
+
success_flag = False
|
|
182
|
+
prev_seg_end = int(0) if seg_index == 0 else self.segments[seg_index-1].target_timerange.end
|
|
183
|
+
next_seg_start = int(1e15) if seg_index == len(self.segments)-1 else self.segments[seg_index+1].start
|
|
184
|
+
for mode in extend:
|
|
185
|
+
if mode == ExtendMode.extend_head:
|
|
186
|
+
if seg.start - delta_duration >= prev_seg_end:
|
|
187
|
+
seg.start -= delta_duration
|
|
188
|
+
success_flag = True
|
|
189
|
+
elif mode == ExtendMode.extend_tail:
|
|
190
|
+
if seg.target_timerange.end + delta_duration <= next_seg_start:
|
|
191
|
+
seg.duration += delta_duration
|
|
192
|
+
success_flag = True
|
|
193
|
+
elif mode == ExtendMode.push_tail:
|
|
194
|
+
shift_duration = max(0, seg.target_timerange.end + delta_duration - next_seg_start)
|
|
195
|
+
seg.duration += delta_duration
|
|
196
|
+
if shift_duration > 0: # 有必要时后移后续片段
|
|
197
|
+
for i in range(seg_index+1, len(self.segments)):
|
|
198
|
+
self.segments[i].start += shift_duration
|
|
199
|
+
success_flag = True
|
|
200
|
+
elif mode == ExtendMode.cut_material_tail:
|
|
201
|
+
src_timerange.duration = seg.duration
|
|
202
|
+
success_flag = True
|
|
203
|
+
else:
|
|
204
|
+
raise ValueError(f"Unsupported extend mode: {mode}")
|
|
205
|
+
|
|
206
|
+
if success_flag:
|
|
207
|
+
break
|
|
208
|
+
if not success_flag:
|
|
209
|
+
raise exceptions.ExtensionFailed(f"未能将片段延长至 {new_duration}μs, 尝试过以下方法: {extend}")
|
|
210
|
+
|
|
211
|
+
# 写入素材时间范围
|
|
212
|
+
seg.source_timerange = src_timerange
|
|
213
|
+
|
|
214
|
+
def import_track(json_data: Dict[str, Any]) -> ImportedTrack:
|
|
215
|
+
"""导入轨道"""
|
|
216
|
+
track_type = TrackType.from_name(json_data["type"])
|
|
217
|
+
if not track_type.value.allow_modify:
|
|
218
|
+
return ImportedTrack(json_data)
|
|
219
|
+
if track_type == TrackType.text:
|
|
220
|
+
return ImportedTextTrack(json_data)
|
|
221
|
+
return ImportedMediaTrack(json_data)
|