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,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
+ }