pycapcut 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyJianYingDraft/__init__.py +276 -0
- pyJianYingDraft/animation.py +140 -0
- pyJianYingDraft/assets/__init__.py +45 -0
- pyJianYingDraft/assets/draft_content_template.json +125 -0
- pyJianYingDraft/assets/draft_meta_info.json +67 -0
- pyJianYingDraft/audio_segment.py +225 -0
- pyJianYingDraft/draft_content_template.json +119 -0
- pyJianYingDraft/draft_folder.py +153 -0
- pyJianYingDraft/effect_segment.py +36 -0
- pyJianYingDraft/exceptions.py +23 -0
- pyJianYingDraft/jianying_controller.py +230 -0
- pyJianYingDraft/keyframe.py +94 -0
- pyJianYingDraft/local_materials.py +198 -0
- pyJianYingDraft/metadata/__init__.py +51 -0
- pyJianYingDraft/metadata/animation_meta.py +689 -0
- pyJianYingDraft/metadata/audio_effect_meta.py +367 -0
- pyJianYingDraft/metadata/audio_scene_effect.py +252 -0
- pyJianYingDraft/metadata/effect_meta.py +191 -0
- pyJianYingDraft/metadata/filter_meta.py +603 -0
- pyJianYingDraft/metadata/font_meta.py +341 -0
- pyJianYingDraft/metadata/mask_meta.py +15 -0
- pyJianYingDraft/metadata/speech_to_song.py +12 -0
- pyJianYingDraft/metadata/text_intro.py +152 -0
- pyJianYingDraft/metadata/text_loop.py +105 -0
- pyJianYingDraft/metadata/text_outro.py +105 -0
- pyJianYingDraft/metadata/tone_effect.py +107 -0
- pyJianYingDraft/metadata/transition_meta.py +733 -0
- pyJianYingDraft/metadata/video_character_effect.py +1913 -0
- pyJianYingDraft/metadata/video_effect_meta.py +9619 -0
- pyJianYingDraft/metadata/video_group_animation.py +131 -0
- pyJianYingDraft/metadata/video_intro.py +103 -0
- pyJianYingDraft/metadata/video_outro.py +80 -0
- pyJianYingDraft/metadata/video_scene_effect.py +7706 -0
- pyJianYingDraft/script_file.py +808 -0
- pyJianYingDraft/segment.py +256 -0
- pyJianYingDraft/template_mode.py +221 -0
- pyJianYingDraft/text_segment.py +436 -0
- pyJianYingDraft/time_util.py +99 -0
- pyJianYingDraft/track.py +138 -0
- pyJianYingDraft/util.py +58 -0
- pyJianYingDraft/video_segment.py +526 -0
- pycapcut/__init__.py +276 -0
- pycapcut/animation.py +140 -0
- pycapcut/assets/__init__.py +45 -0
- pycapcut/assets/draft_content_template.json +132 -0
- pycapcut/assets/draft_meta_info.json +73 -0
- pycapcut/audio_segment.py +225 -0
- pycapcut/draft_folder.py +153 -0
- pycapcut/effect_segment.py +36 -0
- pycapcut/exceptions.py +23 -0
- pycapcut/jianying_controller.py +230 -0
- pycapcut/keyframe.py +94 -0
- pycapcut/local_materials.py +198 -0
- pycapcut/metadata/__init__.py +51 -0
- pycapcut/metadata/audio_scene_effect.py +252 -0
- pycapcut/metadata/effect_meta.py +191 -0
- pycapcut/metadata/filter_meta.py +603 -0
- pycapcut/metadata/font_meta.py +341 -0
- pycapcut/metadata/mask_meta.py +15 -0
- pycapcut/metadata/speech_to_song.py +12 -0
- pycapcut/metadata/text_intro.py +152 -0
- pycapcut/metadata/text_loop.py +105 -0
- pycapcut/metadata/text_outro.py +105 -0
- pycapcut/metadata/tone_effect.py +107 -0
- pycapcut/metadata/transition_meta.py +733 -0
- pycapcut/metadata/video_character_effect.py +1913 -0
- pycapcut/metadata/video_group_animation.py +131 -0
- pycapcut/metadata/video_intro.py +103 -0
- pycapcut/metadata/video_outro.py +80 -0
- pycapcut/metadata/video_scene_effect.py +7706 -0
- pycapcut/script_file.py +808 -0
- pycapcut/segment.py +256 -0
- pycapcut/template_mode.py +221 -0
- pycapcut/text_segment.py +436 -0
- pycapcut/time_util.py +99 -0
- pycapcut/track.py +138 -0
- pycapcut/util.py +58 -0
- pycapcut/video_segment.py +526 -0
- pycapcut-0.0.1.dist-info/METADATA +77 -0
- pycapcut-0.0.1.dist-info/RECORD +88 -0
- pycapcut-0.0.1.dist-info/WHEEL +5 -0
- pycapcut-0.0.1.dist-info/top_level.txt +1 -0
- tools/__init__.py +17 -0
- tools/build_release.py +270 -0
- tools/code_generator.py +247 -0
- tools/extract_all.py +256 -0
- tools/extractor_base.py +132 -0
- tools/jianying_extractor.py +262 -0
pycapcut/text_segment.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""定义文本片段及其相关类"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Tuple, Any
|
|
8
|
+
from typing import Union, Optional, Literal
|
|
9
|
+
|
|
10
|
+
from .time_util import Timerange, tim
|
|
11
|
+
from .segment import ClipSettings, VisualSegment
|
|
12
|
+
from .animation import SegmentAnimations, Text_animation
|
|
13
|
+
|
|
14
|
+
from .metadata import FontType, EffectMeta
|
|
15
|
+
from .metadata import TextIntro, TextOutro, TextLoopAnim
|
|
16
|
+
|
|
17
|
+
class TextStyle:
|
|
18
|
+
"""字体样式类"""
|
|
19
|
+
|
|
20
|
+
size: float
|
|
21
|
+
"""字体大小"""
|
|
22
|
+
|
|
23
|
+
bold: bool
|
|
24
|
+
"""是否加粗"""
|
|
25
|
+
italic: bool
|
|
26
|
+
"""是否斜体"""
|
|
27
|
+
underline: bool
|
|
28
|
+
"""是否加下划线"""
|
|
29
|
+
|
|
30
|
+
color: Tuple[float, float, float]
|
|
31
|
+
"""字体颜色, RGB三元组, 取值范围为[0, 1]"""
|
|
32
|
+
alpha: float
|
|
33
|
+
"""字体不透明度"""
|
|
34
|
+
|
|
35
|
+
align: Literal[0, 1, 2]
|
|
36
|
+
"""对齐方式"""
|
|
37
|
+
vertical: bool
|
|
38
|
+
"""是否为竖排文本"""
|
|
39
|
+
|
|
40
|
+
letter_spacing: int
|
|
41
|
+
"""字符间距"""
|
|
42
|
+
line_spacing: int
|
|
43
|
+
"""行间距"""
|
|
44
|
+
|
|
45
|
+
auto_wrapping: bool
|
|
46
|
+
"""是否自动换行"""
|
|
47
|
+
max_line_width: float
|
|
48
|
+
"""最大行宽, 取值范围为[0, 1]"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, *, size: float = 8.0, bold: bool = False, italic: bool = False, underline: bool = False,
|
|
51
|
+
color: Tuple[float, float, float] = (1.0, 1.0, 1.0), alpha: float = 1.0,
|
|
52
|
+
align: Literal[0, 1, 2] = 0, vertical: bool = False,
|
|
53
|
+
letter_spacing: int = 0, line_spacing: int = 0,
|
|
54
|
+
auto_wrapping: bool = False, max_line_width: float = 0.82):
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
size (`float`, optional): 字体大小, 默认为8.0
|
|
58
|
+
bold (`bool`, optional): 是否加粗, 默认为否
|
|
59
|
+
italic (`bool`, optional): 是否斜体, 默认为否
|
|
60
|
+
underline (`bool`, optional): 是否加下划线, 默认为否
|
|
61
|
+
color (`Tuple[float, float, float]`, optional): 字体颜色, RGB三元组, 取值范围为[0, 1], 默认为白色
|
|
62
|
+
alpha (`float`, optional): 字体不透明度, 取值范围[0, 1], 默认不透明
|
|
63
|
+
align (`int`, optional): 对齐方式, 0: 左对齐, 1: 居中, 2: 右对齐, 默认为左对齐
|
|
64
|
+
vertical (`bool`, optional): 是否为竖排文本, 默认为否
|
|
65
|
+
letter_spacing (`int`, optional): 字符间距, 定义与剪映中一致, 默认为0
|
|
66
|
+
line_spacing (`int`, optional): 行间距, 定义与剪映中一致, 默认为0
|
|
67
|
+
auto_wrapping (`bool`, optional): 是否自动换行, 默认关闭
|
|
68
|
+
max_line_width (`float`, optional): 每行最大行宽占屏幕宽度比例, 取值范围为[0, 1], 默认为0.82
|
|
69
|
+
"""
|
|
70
|
+
self.size = size
|
|
71
|
+
self.bold = bold
|
|
72
|
+
self.italic = italic
|
|
73
|
+
self.underline = underline
|
|
74
|
+
|
|
75
|
+
self.color = color
|
|
76
|
+
self.alpha = alpha
|
|
77
|
+
|
|
78
|
+
self.align = align
|
|
79
|
+
self.vertical = vertical
|
|
80
|
+
|
|
81
|
+
self.letter_spacing = letter_spacing
|
|
82
|
+
self.line_spacing = line_spacing
|
|
83
|
+
|
|
84
|
+
self.auto_wrapping = auto_wrapping
|
|
85
|
+
self.max_line_width = max_line_width
|
|
86
|
+
|
|
87
|
+
class TextBorder:
|
|
88
|
+
"""文本描边的参数"""
|
|
89
|
+
|
|
90
|
+
alpha: float
|
|
91
|
+
"""描边不透明度"""
|
|
92
|
+
color: Tuple[float, float, float]
|
|
93
|
+
"""描边颜色, RGB三元组, 取值范围为[0, 1]"""
|
|
94
|
+
width: float
|
|
95
|
+
"""描边宽度"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, *, alpha: float = 1.0, color: Tuple[float, float, float] = (0.0, 0.0, 0.0), width: float = 40.0):
|
|
98
|
+
"""
|
|
99
|
+
Args:
|
|
100
|
+
alpha (`float`, optional): 描边不透明度, 取值范围[0, 1], 默认为1.0
|
|
101
|
+
color (`Tuple[float, float, float]`, optional): 描边颜色, RGB三元组, 取值范围为[0, 1], 默认为黑色
|
|
102
|
+
width (`float`, optional): 描边宽度, 与剪映中一致, 取值范围为[0, 100], 默认为40.0
|
|
103
|
+
"""
|
|
104
|
+
self.alpha = alpha
|
|
105
|
+
self.color = color
|
|
106
|
+
self.width = width / 100.0 * 0.2 # 此映射可能不完全正确
|
|
107
|
+
|
|
108
|
+
def export_json(self) -> Dict[str, Any]:
|
|
109
|
+
"""导出JSON数据, 放置在素材content的styles中"""
|
|
110
|
+
return {
|
|
111
|
+
"content": {
|
|
112
|
+
"solid": {
|
|
113
|
+
"alpha": self.alpha,
|
|
114
|
+
"color": list(self.color),
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"width": self.width
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class TextBackground:
|
|
121
|
+
"""文本背景参数"""
|
|
122
|
+
|
|
123
|
+
style: Literal[1, 2]
|
|
124
|
+
"""背景样式"""
|
|
125
|
+
|
|
126
|
+
alpha: float
|
|
127
|
+
"""背景不透明度"""
|
|
128
|
+
color: str
|
|
129
|
+
"""背景颜色, 格式为'#RRGGBB'"""
|
|
130
|
+
round_radius: float
|
|
131
|
+
"""背景圆角半径"""
|
|
132
|
+
height: float
|
|
133
|
+
"""背景高度"""
|
|
134
|
+
width: float
|
|
135
|
+
"""背景宽度"""
|
|
136
|
+
horizontal_offset: float
|
|
137
|
+
"""背景水平偏移"""
|
|
138
|
+
vertical_offset: float
|
|
139
|
+
"""背景竖直偏移"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, *, color: str, style: Literal[1, 2] = 1, alpha: float = 1.0, round_radius: float = 0.0,
|
|
142
|
+
height: float = 0.14, width: float = 0.14,
|
|
143
|
+
horizontal_offset: float = 0.5, vertical_offset: float = 0.5):
|
|
144
|
+
"""
|
|
145
|
+
Args:
|
|
146
|
+
color (`str`): 背景颜色, 格式为'#RRGGBB'
|
|
147
|
+
style (`int`, optional): 背景样式, 1和2分别对应剪映中的两种样式, 默认为1
|
|
148
|
+
alpha (`float`, optional): 背景不透明度, 与剪映中一致, 取值范围[0, 1], 默认为1.0
|
|
149
|
+
round_radius (`float`, optional): 背景圆角半径, 与剪映中一致, 取值范围[0, 1], 默认为0.0
|
|
150
|
+
height (`float`, optional): 背景高度, 与剪映中一致, 取值范围为[0, 1], 默认为0.14
|
|
151
|
+
width (`float`, optional): 背景宽度, 与剪映中一致, 取值范围为[0, 1], 默认为0.14
|
|
152
|
+
horizontal_offset (`float`, optional): 背景水平偏移, 与剪映中一致, 取值范围为[0, 1], 默认为0.5
|
|
153
|
+
vertical_offset (`float`, optional): 背景竖直偏移, 与剪映中一致, 取值范围为[0, 1], 默认为0.5
|
|
154
|
+
"""
|
|
155
|
+
self.style = style
|
|
156
|
+
|
|
157
|
+
self.alpha = alpha
|
|
158
|
+
self.color = color
|
|
159
|
+
self.round_radius = round_radius
|
|
160
|
+
self.height = height
|
|
161
|
+
self.width = width
|
|
162
|
+
self.horizontal_offset = horizontal_offset * 2 - 1
|
|
163
|
+
self.vertical_offset = vertical_offset * 2 - 1
|
|
164
|
+
|
|
165
|
+
def export_json(self) -> Dict[str, Any]:
|
|
166
|
+
"""生成子JSON数据, 在TextSegment导出时合并到其中"""
|
|
167
|
+
return {
|
|
168
|
+
"background_style": self.style,
|
|
169
|
+
"background_color": self.color,
|
|
170
|
+
"background_alpha": self.alpha,
|
|
171
|
+
"background_round_radius": self.round_radius,
|
|
172
|
+
"background_height": self.height,
|
|
173
|
+
"background_width": self.width,
|
|
174
|
+
"background_horizontal_offset": self.horizontal_offset,
|
|
175
|
+
"background_vertical_offset": self.vertical_offset,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
class TextBubble:
|
|
179
|
+
"""文本气泡素材, 与滤镜素材本质上一致"""
|
|
180
|
+
|
|
181
|
+
global_id: str
|
|
182
|
+
"""气泡全局id, 由程序自动生成"""
|
|
183
|
+
|
|
184
|
+
effect_id: str
|
|
185
|
+
resource_id: str
|
|
186
|
+
|
|
187
|
+
def __init__(self, effect_id: str, resource_id: str):
|
|
188
|
+
self.global_id = uuid.uuid4().hex
|
|
189
|
+
self.effect_id = effect_id
|
|
190
|
+
self.resource_id = resource_id
|
|
191
|
+
|
|
192
|
+
def export_json(self) -> Dict[str, Any]:
|
|
193
|
+
return {
|
|
194
|
+
"apply_target_type": 0,
|
|
195
|
+
"effect_id": self.effect_id,
|
|
196
|
+
"id": self.global_id,
|
|
197
|
+
"resource_id": self.resource_id,
|
|
198
|
+
"type": "text_shape",
|
|
199
|
+
"value": 1.0,
|
|
200
|
+
# 不导出path和request_id
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class TextEffect(TextBubble):
|
|
204
|
+
"""文本花字素材, 与滤镜素材本质上也一致"""
|
|
205
|
+
|
|
206
|
+
def export_json(self) -> Dict[str, Any]:
|
|
207
|
+
ret = super().export_json()
|
|
208
|
+
ret["type"] = "text_effect"
|
|
209
|
+
ret["source_platform"] = 1
|
|
210
|
+
return ret
|
|
211
|
+
|
|
212
|
+
class TextSegment(VisualSegment):
|
|
213
|
+
"""文本片段类, 目前仅支持设置基本的字体样式"""
|
|
214
|
+
|
|
215
|
+
text: str
|
|
216
|
+
"""文本内容"""
|
|
217
|
+
font: Optional[EffectMeta]
|
|
218
|
+
"""字体类型"""
|
|
219
|
+
style: TextStyle
|
|
220
|
+
"""字体样式"""
|
|
221
|
+
|
|
222
|
+
border: Optional[TextBorder]
|
|
223
|
+
"""文本描边参数, None表示无描边"""
|
|
224
|
+
background: Optional[TextBackground]
|
|
225
|
+
"""文本背景参数, None表示无背景"""
|
|
226
|
+
|
|
227
|
+
bubble: Optional[TextBubble]
|
|
228
|
+
"""文本气泡效果, 在放入轨道时加入素材列表中"""
|
|
229
|
+
effect: Optional[TextEffect]
|
|
230
|
+
"""文本花字效果, 在放入轨道时加入素材列表中, 目前仅支持一部分花字效果"""
|
|
231
|
+
|
|
232
|
+
def __init__(self, text: str, timerange: Timerange, *,
|
|
233
|
+
font: Optional[FontType] = None,
|
|
234
|
+
style: Optional[TextStyle] = None, clip_settings: Optional[ClipSettings] = None,
|
|
235
|
+
border: Optional[TextBorder] = None, background: Optional[TextBackground] = None):
|
|
236
|
+
"""创建文本片段, 并指定其时间信息、字体样式及图像调节设置
|
|
237
|
+
|
|
238
|
+
片段创建完成后, 可通过`ScriptFile.add_segment`方法将其添加到轨道中
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
text (`str`): 文本内容
|
|
242
|
+
timerange (`Timerange`): 片段在轨道上的时间范围
|
|
243
|
+
font (`Font_type`, optional): 字体类型, 默认为系统字体
|
|
244
|
+
style (`TextStyle`, optional): 字体样式, 包含大小/颜色/对齐/透明度等.
|
|
245
|
+
clip_settings (`ClipSettings`, optional): 图像调节设置, 默认不做任何变换
|
|
246
|
+
border (`TextBorder`, optional): 文本描边参数, 默认无描边
|
|
247
|
+
background (`TextBackground`, optional): 文本背景参数, 默认无背景
|
|
248
|
+
"""
|
|
249
|
+
super().__init__(uuid.uuid4().hex, None, timerange, 1.0, 1.0, clip_settings=clip_settings)
|
|
250
|
+
|
|
251
|
+
self.text = text
|
|
252
|
+
self.font = font.value if font else None
|
|
253
|
+
self.style = style or TextStyle()
|
|
254
|
+
self.border = border
|
|
255
|
+
self.background = background
|
|
256
|
+
|
|
257
|
+
self.bubble = None
|
|
258
|
+
self.effect = None
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def create_from_template(cls, text: str, timerange: Timerange, template: "TextSegment") -> "TextSegment":
|
|
262
|
+
"""根据模板创建新的文本片段, 并指定其文本内容"""
|
|
263
|
+
new_segment = cls(text, timerange, style=deepcopy(template.style), clip_settings=deepcopy(template.clip_settings),
|
|
264
|
+
border=deepcopy(template.border), background=deepcopy(template.background))
|
|
265
|
+
new_segment.font = deepcopy(template.font)
|
|
266
|
+
|
|
267
|
+
# 处理动画等
|
|
268
|
+
if template.animations_instance:
|
|
269
|
+
new_segment.animations_instance = deepcopy(template.animations_instance)
|
|
270
|
+
new_segment.animations_instance.animation_id = uuid.uuid4().hex
|
|
271
|
+
new_segment.extra_material_refs.append(new_segment.animations_instance.animation_id)
|
|
272
|
+
if template.bubble:
|
|
273
|
+
new_segment.add_bubble(template.bubble.effect_id, template.bubble.resource_id)
|
|
274
|
+
if template.effect:
|
|
275
|
+
new_segment.add_effect(template.effect.effect_id)
|
|
276
|
+
|
|
277
|
+
return new_segment
|
|
278
|
+
|
|
279
|
+
def add_animation(self, animation_type: Union[TextIntro, TextOutro, TextLoopAnim],
|
|
280
|
+
duration: Union[str, float] = 500000) -> "TextSegment":
|
|
281
|
+
"""将给定的入场/出场/循环动画添加到此片段的动画列表中, 出入场动画的持续时间可以自行设置, 循环动画则会自动填满其余无动画部分
|
|
282
|
+
|
|
283
|
+
注意: 若希望同时使用循环动画和入出场动画, 请**先添加出入场动画再添加循环动画**
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
animation_type (`TextIntro`, `TextOutro` or `TextLoopAnim`): 文本动画类型.
|
|
287
|
+
duration (`str` or `float`, optional): 动画持续时间, 单位为微秒, 仅对入场/出场动画有效.
|
|
288
|
+
若传入字符串则会调用`tim()`函数进行解析. 默认为0.5秒
|
|
289
|
+
"""
|
|
290
|
+
duration = min(tim(duration), self.target_timerange.duration)
|
|
291
|
+
|
|
292
|
+
if isinstance(animation_type, TextIntro):
|
|
293
|
+
start = 0
|
|
294
|
+
elif isinstance(animation_type, TextOutro):
|
|
295
|
+
start = self.target_timerange.duration - duration
|
|
296
|
+
elif isinstance(animation_type, TextLoopAnim):
|
|
297
|
+
intro_trange = self.animations_instance and self.animations_instance.get_animation_trange("in")
|
|
298
|
+
outro_trange = self.animations_instance and self.animations_instance.get_animation_trange("out")
|
|
299
|
+
start = intro_trange.start if intro_trange else 0
|
|
300
|
+
duration = self.target_timerange.duration - start - (outro_trange.duration if outro_trange else 0)
|
|
301
|
+
else:
|
|
302
|
+
raise TypeError("Invalid animation type %s" % type(animation_type))
|
|
303
|
+
|
|
304
|
+
if self.animations_instance is None:
|
|
305
|
+
self.animations_instance = SegmentAnimations()
|
|
306
|
+
self.extra_material_refs.append(self.animations_instance.animation_id)
|
|
307
|
+
|
|
308
|
+
self.animations_instance.add_animation(Text_animation(animation_type, start, duration))
|
|
309
|
+
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
def add_bubble(self, effect_id: str, resource_id: str) -> "TextSegment":
|
|
313
|
+
"""根据素材信息添加气泡效果, 相应素材信息可通过`ScriptFile.inspect_material`从模板中获取
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
effect_id (`str`): 气泡效果的effect_id
|
|
317
|
+
resource_id (`str`): 气泡效果的resource_id
|
|
318
|
+
"""
|
|
319
|
+
self.bubble = TextBubble(effect_id, resource_id)
|
|
320
|
+
self.extra_material_refs.append(self.bubble.global_id)
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
def add_effect(self, effect_id: str) -> "TextSegment":
|
|
324
|
+
"""根据素材信息添加花字效果, 相应素材信息可通过`ScriptFile.inspect_material`从模板中获取
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
effect_id (`str`): 花字效果的effect_id, 也同时是其resource_id
|
|
328
|
+
"""
|
|
329
|
+
self.effect = TextEffect(effect_id, effect_id)
|
|
330
|
+
self.extra_material_refs.append(self.effect.global_id)
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def export_material(self) -> Dict[str, Any]:
|
|
334
|
+
"""与此文本片段联系的素材, 以此不再单独定义Text_material类"""
|
|
335
|
+
# 叠加各类效果的flag
|
|
336
|
+
check_flag: int = 7
|
|
337
|
+
if self.border:
|
|
338
|
+
check_flag |= 8
|
|
339
|
+
if self.background:
|
|
340
|
+
check_flag |= 16
|
|
341
|
+
|
|
342
|
+
content_json = {
|
|
343
|
+
"styles": [
|
|
344
|
+
{
|
|
345
|
+
"fill": {
|
|
346
|
+
"alpha": 1.0,
|
|
347
|
+
"content": {
|
|
348
|
+
"render_type": "solid",
|
|
349
|
+
"solid": {
|
|
350
|
+
"alpha": 1.0,
|
|
351
|
+
"color": list(self.style.color)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
"range": [0, len(self.text)],
|
|
356
|
+
"size": self.style.size,
|
|
357
|
+
"bold": self.style.bold,
|
|
358
|
+
"italic": self.style.italic,
|
|
359
|
+
"underline": self.style.underline,
|
|
360
|
+
"strokes": [self.border.export_json()] if self.border else []
|
|
361
|
+
}
|
|
362
|
+
],
|
|
363
|
+
"text": self.text
|
|
364
|
+
}
|
|
365
|
+
if self.font:
|
|
366
|
+
content_json["styles"][0]["font"] = {
|
|
367
|
+
"id": self.font.resource_id,
|
|
368
|
+
"path": "C:/%s.ttf" % self.font.name # 并不会真正在此处放置字体文件
|
|
369
|
+
}
|
|
370
|
+
if self.effect:
|
|
371
|
+
content_json["styles"][0]["effectStyle"] = {
|
|
372
|
+
"id": self.effect.effect_id,
|
|
373
|
+
"path": "C:" # 并不会真正在此处放置素材文件
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
ret = {
|
|
377
|
+
"id": self.material_id,
|
|
378
|
+
"content": json.dumps(content_json, ensure_ascii=False),
|
|
379
|
+
|
|
380
|
+
"typesetting": int(self.style.vertical),
|
|
381
|
+
"alignment": self.style.align,
|
|
382
|
+
"letter_spacing": self.style.letter_spacing * 0.05,
|
|
383
|
+
"line_spacing": 0.02 + self.style.line_spacing * 0.05,
|
|
384
|
+
|
|
385
|
+
"line_feed": 1,
|
|
386
|
+
"line_max_width": self.style.max_line_width,
|
|
387
|
+
"force_apply_line_max_width": False,
|
|
388
|
+
|
|
389
|
+
"check_flag": check_flag,
|
|
390
|
+
|
|
391
|
+
"type": "subtitle" if self.style.auto_wrapping else "text",
|
|
392
|
+
|
|
393
|
+
# 混合 (+4)
|
|
394
|
+
"global_alpha": self.style.alpha,
|
|
395
|
+
|
|
396
|
+
# 发光 (+64),属性由extra_material_refs记录
|
|
397
|
+
|
|
398
|
+
# 阴影 (+32)
|
|
399
|
+
# "has_shadow": False,
|
|
400
|
+
# "shadow_alpha": 0.9,
|
|
401
|
+
# "shadow_angle": -45.0,
|
|
402
|
+
# "shadow_color": "",
|
|
403
|
+
# "shadow_distance": 5.0,
|
|
404
|
+
# "shadow_point": {
|
|
405
|
+
# "x": 0.6363961030678928,
|
|
406
|
+
# "y": -0.6363961030678928
|
|
407
|
+
# },
|
|
408
|
+
# "shadow_smoothing": 0.45,
|
|
409
|
+
|
|
410
|
+
# 整体字体设置, 似乎会被content覆盖
|
|
411
|
+
# "font_category_id": "",
|
|
412
|
+
# "font_category_name": "",
|
|
413
|
+
# "font_id": "",
|
|
414
|
+
# "font_name": "",
|
|
415
|
+
# "font_path": "",
|
|
416
|
+
# "font_resource_id": "",
|
|
417
|
+
# "font_size": 15.0,
|
|
418
|
+
# "font_source_platform": 0,
|
|
419
|
+
# "font_team_id": "",
|
|
420
|
+
# "font_title": "none",
|
|
421
|
+
# "font_url": "",
|
|
422
|
+
# "fonts": [],
|
|
423
|
+
|
|
424
|
+
# 似乎会被content覆盖
|
|
425
|
+
# "text_alpha": 1.0,
|
|
426
|
+
# "text_color": "#FFFFFF",
|
|
427
|
+
# "text_curve": None,
|
|
428
|
+
# "text_preset_resource_id": "",
|
|
429
|
+
# "text_size": 30,
|
|
430
|
+
# "underline": False,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if self.background:
|
|
434
|
+
ret.update(self.background.export_json())
|
|
435
|
+
|
|
436
|
+
return ret
|
pycapcut/time_util.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""定义时间范围类以及与时间相关的辅助函数"""
|
|
2
|
+
|
|
3
|
+
from typing import Union
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
SEC = 1000000
|
|
7
|
+
"""一秒=1e6微秒"""
|
|
8
|
+
|
|
9
|
+
def tim(inp: Union[str, float]) -> int:
|
|
10
|
+
"""将输入的字符串转换为微秒, 也可直接输入微秒数
|
|
11
|
+
|
|
12
|
+
支持类似 "1h52m3s" 或 "0.15s" 这样的格式, 可包含负号以表示负偏移
|
|
13
|
+
"""
|
|
14
|
+
if isinstance(inp, (int, float)):
|
|
15
|
+
return int(round(inp))
|
|
16
|
+
|
|
17
|
+
sign: int = 1
|
|
18
|
+
inp = inp.strip().lower()
|
|
19
|
+
if inp.startswith("-"):
|
|
20
|
+
sign = -1
|
|
21
|
+
inp = inp[1:]
|
|
22
|
+
|
|
23
|
+
last_index: int = 0
|
|
24
|
+
total_time: float = 0
|
|
25
|
+
for unit, factor in zip(["h", "m", "s"], [3600*SEC, 60*SEC, SEC]):
|
|
26
|
+
unit_index = inp.find(unit)
|
|
27
|
+
if unit_index == -1: continue
|
|
28
|
+
|
|
29
|
+
total_time += float(inp[last_index:unit_index]) * factor
|
|
30
|
+
last_index = unit_index + 1
|
|
31
|
+
|
|
32
|
+
return int(round(total_time) * sign)
|
|
33
|
+
|
|
34
|
+
class Timerange:
|
|
35
|
+
"""记录了起始时间及持续长度的时间范围"""
|
|
36
|
+
start: int
|
|
37
|
+
"""起始时间, 单位为微秒"""
|
|
38
|
+
duration: int
|
|
39
|
+
"""持续长度, 单位为微秒"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, start: int, duration: int):
|
|
42
|
+
"""构造一个时间范围
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
start (int): 起始时间, 单位为微秒
|
|
46
|
+
duration (int): 持续长度, 单位为微秒
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
self.start = start
|
|
50
|
+
self.duration = duration
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def import_json(cls, json_obj: Dict[str, str]) -> "Timerange":
|
|
54
|
+
"""从json对象中恢复Timerange"""
|
|
55
|
+
return cls(int(json_obj["start"]), int(json_obj["duration"]))
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def end(self) -> int:
|
|
59
|
+
"""结束时间, 单位为微秒"""
|
|
60
|
+
return self.start + self.duration
|
|
61
|
+
|
|
62
|
+
def __eq__(self, other: object) -> bool:
|
|
63
|
+
if not isinstance(other, Timerange):
|
|
64
|
+
return False
|
|
65
|
+
return self.start == other.start and self.duration == other.duration
|
|
66
|
+
|
|
67
|
+
def overlaps(self, other: "Timerange") -> bool:
|
|
68
|
+
"""判断两个时间范围是否有重叠"""
|
|
69
|
+
return not (self.end <= other.start or other.end <= self.start)
|
|
70
|
+
|
|
71
|
+
def __repr__(self) -> str:
|
|
72
|
+
return f"Timerange(start={self.start}, duration={self.duration})"
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
return f"[start={self.start}, end={self.end}]"
|
|
76
|
+
|
|
77
|
+
def export_json(self) -> Dict[str, int]:
|
|
78
|
+
return {"start": self.start, "duration": self.duration}
|
|
79
|
+
|
|
80
|
+
def trange(start: Union[str, float], duration: Union[str, float]) -> Timerange:
|
|
81
|
+
"""Timerange的简便构造函数, 接受字符串或微秒数作为参数
|
|
82
|
+
|
|
83
|
+
支持类似 "1h52m3s" 或 "0.15s" 这样的格式
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
start (Union[str, float]): 起始时间
|
|
87
|
+
duration (Union[str, float]): 持续长度, 注意**不是结束时间**
|
|
88
|
+
"""
|
|
89
|
+
return Timerange(tim(start), tim(duration))
|
|
90
|
+
|
|
91
|
+
def srt_tstamp(srt_tstamp: str) -> int:
|
|
92
|
+
"""解析srt中的时间戳字符串, 返回微秒数"""
|
|
93
|
+
sec_str, ms_str = srt_tstamp.split(",")
|
|
94
|
+
parts = sec_str.split(":") + [ms_str]
|
|
95
|
+
|
|
96
|
+
total_time = 0
|
|
97
|
+
for value, factor in zip(parts, [3600*SEC, 60*SEC, SEC, 1000]):
|
|
98
|
+
total_time += int(value) * factor
|
|
99
|
+
return total_time
|
pycapcut/track.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""轨道类及其元数据"""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import TypeVar, Generic, Type
|
|
7
|
+
from typing import Dict, List, Any, Union
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
from .exceptions import SegmentOverlap
|
|
12
|
+
from .segment import BaseSegment
|
|
13
|
+
from .video_segment import VideoSegment, StickerSegment
|
|
14
|
+
from .audio_segment import AudioSegment
|
|
15
|
+
from .text_segment import TextSegment
|
|
16
|
+
from .effect_segment import EffectSegment, FilterSegment
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Track_meta:
|
|
20
|
+
"""与轨道类型关联的轨道元数据"""
|
|
21
|
+
|
|
22
|
+
segment_type: Union[Type[VideoSegment], Type[AudioSegment],
|
|
23
|
+
Type[EffectSegment], Type[FilterSegment],
|
|
24
|
+
Type[TextSegment], Type[StickerSegment], None]
|
|
25
|
+
"""与轨道关联的片段类型"""
|
|
26
|
+
render_index: int
|
|
27
|
+
"""默认渲染顺序, 值越大越接近前景"""
|
|
28
|
+
allow_modify: bool
|
|
29
|
+
"""当被导入时, 是否允许修改"""
|
|
30
|
+
|
|
31
|
+
class TrackType(Enum):
|
|
32
|
+
"""轨道类型枚举
|
|
33
|
+
|
|
34
|
+
变量名对应type属性, 值表示相应的轨道元数据
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
video = Track_meta(VideoSegment, 0, True)
|
|
38
|
+
audio = Track_meta(AudioSegment, 0, True)
|
|
39
|
+
effect = Track_meta(EffectSegment, 10000, False)
|
|
40
|
+
filter = Track_meta(FilterSegment, 11000, False)
|
|
41
|
+
sticker = Track_meta(StickerSegment, 14000, False)
|
|
42
|
+
text = Track_meta(TextSegment, 15000, True) # 原本是14000, 避免与sticker冲突改为15000
|
|
43
|
+
|
|
44
|
+
adjust = Track_meta(None, 0, False)
|
|
45
|
+
"""仅供导入时使用, 不要尝试新建此类型的轨道"""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def from_name(name: str) -> "TrackType":
|
|
49
|
+
"""根据名称获取轨道类型枚举"""
|
|
50
|
+
for t in TrackType:
|
|
51
|
+
if t.name == name:
|
|
52
|
+
return t
|
|
53
|
+
raise ValueError("Invalid track type: %s" % name)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BaseTrack(ABC):
|
|
57
|
+
"""轨道基类"""
|
|
58
|
+
|
|
59
|
+
track_type: TrackType
|
|
60
|
+
"""轨道类型"""
|
|
61
|
+
name: str
|
|
62
|
+
"""轨道名称"""
|
|
63
|
+
track_id: str
|
|
64
|
+
"""轨道全局ID"""
|
|
65
|
+
render_index: int
|
|
66
|
+
"""渲染顺序, 值越大越接近前景"""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def export_json(self) -> Dict[str, Any]: ...
|
|
70
|
+
|
|
71
|
+
Seg_type = TypeVar("Seg_type", bound=BaseSegment)
|
|
72
|
+
class Track(BaseTrack, Generic[Seg_type]):
|
|
73
|
+
"""非模板模式下的轨道"""
|
|
74
|
+
|
|
75
|
+
mute: bool
|
|
76
|
+
"""是否静音"""
|
|
77
|
+
|
|
78
|
+
segments: List[Seg_type]
|
|
79
|
+
"""该轨道包含的片段列表"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, track_type: TrackType, name: str, render_index: int, mute: bool):
|
|
82
|
+
self.track_type = track_type
|
|
83
|
+
self.name = name
|
|
84
|
+
self.track_id = uuid.uuid4().hex
|
|
85
|
+
self.render_index = render_index
|
|
86
|
+
|
|
87
|
+
self.mute = mute
|
|
88
|
+
self.segments = []
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def end_time(self) -> int:
|
|
92
|
+
"""轨道结束时间, 微秒"""
|
|
93
|
+
if len(self.segments) == 0:
|
|
94
|
+
return 0
|
|
95
|
+
return self.segments[-1].target_timerange.end
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def accept_segment_type(self) -> Type[Seg_type]:
|
|
99
|
+
"""返回该轨道允许的片段类型"""
|
|
100
|
+
return self.track_type.value.segment_type # type: ignore
|
|
101
|
+
|
|
102
|
+
def add_segment(self, segment: Seg_type) -> "Track[Seg_type]":
|
|
103
|
+
"""向轨道中添加一个片段, 添加的片段必须匹配轨道类型且不与现有片段重叠
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
segment (Seg_type): 要添加的片段
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
`TypeError`: 新片段类型与轨道类型不匹配
|
|
110
|
+
`SegmentOverlap`: 新片段与现有片段重叠
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(segment, self.accept_segment_type):
|
|
113
|
+
raise TypeError("New segment (%s) is not of the same type as the track (%s)" % (type(segment), self.accept_segment_type))
|
|
114
|
+
|
|
115
|
+
# 检查片段是否重叠
|
|
116
|
+
for seg in self.segments:
|
|
117
|
+
if seg.overlaps(segment):
|
|
118
|
+
raise SegmentOverlap("New segment overlaps with existing segment [start: {}, end: {}]"
|
|
119
|
+
.format(segment.target_timerange.start, segment.target_timerange.end))
|
|
120
|
+
|
|
121
|
+
self.segments.append(segment)
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def export_json(self) -> Dict[str, Any]:
|
|
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
|
+
|
|
130
|
+
return {
|
|
131
|
+
"attribute": int(self.mute),
|
|
132
|
+
"flag": 0,
|
|
133
|
+
"id": self.track_id,
|
|
134
|
+
"is_default_name": len(self.name) == 0,
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"segments": segment_exports,
|
|
137
|
+
"type": self.track_type.name
|
|
138
|
+
}
|