jianying-draft 1.0.0__tar.gz

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.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: jianying-draft
3
+ Version: 1.0.0
4
+ Summary: utility
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: cryptography>=41.0.0
@@ -0,0 +1,488 @@
1
+ """
2
+ zcatwx-zil6sm6h-wyaaz
3
+ ======================
4
+
5
+ 公开 API:
6
+ generate_draft_json(draft_context) -> str 把 DraftContext 还原为加密的剪映草稿 JSON
7
+ DraftCrypto.encrypt(plaintext) -> str AES-GCM 加密
8
+ DraftCrypto.decrypt(ciphertext) -> str AES-GCM 解密
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import base64
14
+ import json
15
+ import os
16
+ import sys
17
+
18
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
19
+
20
+ # ── OOP 核心 ──────────────────────────────────────────────────────────────────
21
+ from zcatwx_zil6sm6h_wyaaz._core import (
22
+ JianYingDraft, VideoClip, AudioClip, TextClip,
23
+ StickerClip, EffectClip, FilterClip,
24
+ )
25
+
26
+ # ── 字体名称 → ID 查找表 ──────────────────────────────────────────────────────
27
+ def _load_font_name_to_id():
28
+ db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "font_db.json")
29
+ try:
30
+ with open(db_path, "r", encoding="utf-8") as f:
31
+ db = json.load(f)
32
+ return {name: fid for fid, name in db.items()}
33
+ except Exception:
34
+ return {}
35
+
36
+ _FONT_NAME_TO_ID = _load_font_name_to_id()
37
+
38
+
39
+ def _resolve_font_id(style: dict):
40
+ fid = style.get("font_id")
41
+ if fid:
42
+ return fid
43
+ fname = style.get("font_name")
44
+ if fname and fname in _FONT_NAME_TO_ID:
45
+ return _FONT_NAME_TO_ID[fname]
46
+ return None
47
+
48
+
49
+ # ── 加解密 ────────────────────────────────────────────────────────────────────
50
+ class DraftCrypto:
51
+ """AES-GCM 加解密(与 Coze 节点保持相同密钥)"""
52
+
53
+ _KEY = "MySecretKeyForCozeNode20260307!!".encode("utf-8")
54
+
55
+ @classmethod
56
+ def encrypt(cls, plaintext: str) -> str:
57
+ import os as _os
58
+ aesgcm = AESGCM(cls._KEY)
59
+ iv = _os.urandom(12)
60
+ ciphertext = aesgcm.encrypt(iv, plaintext.encode("utf-8"), None)
61
+ return base64.b64encode(iv + ciphertext).decode("utf-8")
62
+
63
+ @classmethod
64
+ def decrypt(cls, payload: str) -> str:
65
+ try:
66
+ aesgcm = AESGCM(cls._KEY)
67
+ raw = base64.b64decode(payload.encode("utf-8"))
68
+ iv, ciphertext = raw[:12], raw[12:]
69
+ return aesgcm.decrypt(iv, ciphertext, None).decode("utf-8")
70
+ except Exception:
71
+ return payload # 兼容明文透传
72
+
73
+
74
+ # ── 时间解析 ──────────────────────────────────────────────────────────────────
75
+ _UNIT_MULTIPLIER = {"us": 1, "s": 1_000_000}
76
+ _tm = 1 # 由 generate_draft_json() 入口按 time_unit 字段更新
77
+
78
+
79
+ def _parse_time(t) -> int:
80
+ if t is None or t == "auto":
81
+ return 0
82
+ if isinstance(t, str):
83
+ t = t.strip()
84
+ if t.endswith("us"):
85
+ return int(float(t[:-2]))
86
+ elif t.endswith("s"):
87
+ return int(float(t[:-1]) * 1_000_000)
88
+ else:
89
+ return int(float(t) * _tm)
90
+ return int(t * _tm)
91
+
92
+
93
+ # ── 核心:DraftContext → 完整剪映草稿 JSON 字符串 ────────────────────────────
94
+ def generate_draft_json(draft_context) -> str:
95
+ """
96
+ 把 Coze 节点生成的 DraftContext(中间层 JSON)还原为完整的剪映草稿 JSON 字符串。
97
+
98
+ 参数:
99
+ draft_context: str 或 dict
100
+ - str: 明文 JSON 字符串 或 AES-GCM 加密的 base64 密文
101
+ - dict: 已解析的 DraftContext 字典
102
+
103
+ 返回:
104
+ str: 加密后的完整剪映草稿 JSON 字符串(可直接用 DraftCrypto.decrypt 解密)
105
+
106
+ 说明:
107
+ - 素材路径(视频/音频/图片 URL)保留原样,不下载
108
+ - 本地 export_draft.py 负责下载资源并替换路径
109
+ """
110
+ global _tm
111
+
112
+ # 1. 解析输入(支持密文、明文字符串、dict)
113
+ if isinstance(draft_context, str):
114
+ decrypted = DraftCrypto.decrypt(draft_context)
115
+ try:
116
+ data = json.loads(decrypted)
117
+ except json.JSONDecodeError as e:
118
+ raise ValueError(f"DraftContext 解析失败: {e}") from e
119
+ else:
120
+ data = draft_context
121
+
122
+ # 2. 初始化项目
123
+ proj = data.get("project_info", {})
124
+ width = proj.get("width", 1920)
125
+ height = proj.get("height", 1080)
126
+ ratio = proj.get("ratio", "custom")
127
+ save_name = proj.get("save_name", "Coze_Auto_Gen")
128
+
129
+ _tm = _UNIT_MULTIPLIER.get(data.get("time_unit", "us"), 1)
130
+
131
+ draft = JianYingDraft(width=width, height=height, ratio=ratio)
132
+
133
+ # 3. 遍历轨道
134
+ clip_map: dict = {}
135
+ for track_info in data.get("tracks", []):
136
+ track_type = track_info.get("type", "video")
137
+ oop_track_type = {"image": "video", "sticker": "video"}.get(track_type, track_type)
138
+ current_track = draft.add_track(oop_track_type)
139
+
140
+ for clip_data in track_info.get("clips", []):
141
+ clip_type = clip_data.get("type", "video")
142
+ clip_id = clip_data.get("id")
143
+ start = _parse_time(clip_data.get("start"))
144
+ duration = _parse_time(clip_data.get("duration"))
145
+ clip_obj = None
146
+
147
+ # ── A. 视频 / 图片 ──────────────────────────────────────────
148
+ if clip_type in ("video", "image"):
149
+ path = clip_data.get("path")
150
+ if not path:
151
+ continue
152
+ source_start = _parse_time(clip_data.get("source_start", 0))
153
+ clip_obj = VideoClip(path, duration=duration, source_start=source_start, start=start)
154
+
155
+ speed_cfg = clip_data.get("speed")
156
+ if speed_cfg:
157
+ if isinstance(speed_cfg, dict):
158
+ clip_obj.set_speed(speed_cfg.get("value", 1.0),
159
+ change_pitch=speed_cfg.get("change_pitch", False))
160
+ else:
161
+ clip_obj.set_speed(float(speed_cfg))
162
+
163
+ transition = clip_data.get("transition")
164
+ if transition:
165
+ clip_obj.add_transition(
166
+ resource_id=transition.get("resource_id"),
167
+ duration=_parse_time(transition.get("duration", 1_000_000)),
168
+ overlap=transition.get("overlap", True),
169
+ )
170
+
171
+ mask = clip_data.get("mask")
172
+ if mask:
173
+ clip_obj.set_mask(
174
+ mask_type=mask.get("type", "rectangle"),
175
+ center_x=mask.get("center_x", 0),
176
+ center_y=mask.get("center_y", 0),
177
+ rotation=mask.get("rotation", 0),
178
+ size_x=mask.get("size_x"),
179
+ size_y=mask.get("size_y"),
180
+ feather=mask.get("feather", 0),
181
+ invert=mask.get("invert", False),
182
+ round_corner=mask.get("round_corner", 0),
183
+ )
184
+
185
+ mix_mode = clip_data.get("mix_mode")
186
+ if mix_mode:
187
+ mode_name = mix_mode if isinstance(mix_mode, str) else mix_mode.get("name", "变亮")
188
+ clip_obj.set_mix_mode(name=mode_name)
189
+
190
+ smart_crop = clip_data.get("smart_crop")
191
+ if smart_crop:
192
+ clip_obj.set_smart_crop(
193
+ ratio=smart_crop.get("ratio", "original"),
194
+ stability=smart_crop.get("stability", 2),
195
+ speed=smart_crop.get("speed", 1),
196
+ )
197
+
198
+ video_algo = clip_data.get("video_algorithm")
199
+ if video_algo:
200
+ clip_obj.set_video_algorithm(
201
+ denoise=video_algo.get("denoise"),
202
+ deflicker=video_algo.get("deflicker"),
203
+ quality_enhance=video_algo.get("quality_enhance"),
204
+ motion_blur=video_algo.get("motion_blur"),
205
+ stabilize=video_algo.get("stabilize"),
206
+ )
207
+
208
+ if clip_data.get("audio_volume") is not None:
209
+ clip_obj.set_volume(clip_data["audio_volume"])
210
+ audio_fade = clip_data.get("audio_fade")
211
+ if audio_fade:
212
+ clip_obj.set_audio_fade(
213
+ fade_in=_parse_time(audio_fade.get("in", 0)),
214
+ fade_out=_parse_time(audio_fade.get("out", 0)),
215
+ )
216
+ if clip_data.get("audio_denoise"):
217
+ clip_obj.set_audio_denoise(True)
218
+ if clip_data.get("loudness"):
219
+ clip_obj.set_loudness(clip_data["loudness"])
220
+ if clip_data.get("vocal_separation") is not None:
221
+ clip_obj.set_vocal_separation(clip_data["vocal_separation"])
222
+ if clip_data.get("vocal_beautify") is not None:
223
+ clip_obj.set_vocal_beautify(clip_data["vocal_beautify"])
224
+ if clip_data.get("tone_effect"):
225
+ clip_obj.set_tone_effect(clip_data["tone_effect"])
226
+ if clip_data.get("color_curves"):
227
+ clip_obj.set_color_curves(True)
228
+
229
+ # ── B. 音频 ─────────────────────────────────────────────────
230
+ elif clip_type == "audio":
231
+ path = clip_data.get("path")
232
+ if not path:
233
+ continue
234
+ source_start = _parse_time(clip_data.get("source_start", 0))
235
+ clip_obj = AudioClip(path, start=start, duration=duration, source_start=source_start)
236
+
237
+ if clip_data.get("volume") is not None:
238
+ clip_obj.set_volume(clip_data["volume"])
239
+
240
+ speed_cfg = clip_data.get("speed")
241
+ if speed_cfg:
242
+ if isinstance(speed_cfg, dict):
243
+ clip_obj.set_speed(speed_cfg.get("value", 1.0),
244
+ change_pitch=speed_cfg.get("change_pitch", False))
245
+ else:
246
+ clip_obj.set_speed(float(speed_cfg))
247
+
248
+ fade = clip_data.get("fade")
249
+ if fade:
250
+ clip_obj.set_fade(
251
+ fade_in=_parse_time(fade.get("in", 0)),
252
+ fade_out=_parse_time(fade.get("out", 0)),
253
+ )
254
+
255
+ if clip_data.get("denoise"):
256
+ clip_obj.set_denoise(True)
257
+ if clip_data.get("loudness"):
258
+ clip_obj.set_loudness(clip_data["loudness"])
259
+ if clip_data.get("vocal_separation") is not None:
260
+ clip_obj.set_vocal_separation(clip_data["vocal_separation"])
261
+ if clip_data.get("vocal_beautify") is not None:
262
+ clip_obj.set_vocal_beautify(clip_data["vocal_beautify"])
263
+
264
+ for ve in clip_data.get("voice_effects", []):
265
+ clip_obj.add_voice_effect(
266
+ resource_id=ve.get("id"),
267
+ type=ve.get("type", "tone"),
268
+ adjust_params=ve.get("params"),
269
+ )
270
+ tone_effect = clip_data.get("tone_effect")
271
+ if tone_effect and not clip_data.get("voice_effects"):
272
+ clip_obj.add_voice_effect(tone_effect, type="tone")
273
+
274
+ # ── C. 文字 ─────────────────────────────────────────────────
275
+ elif clip_type == "text":
276
+ content = clip_data.get("content") or clip_data.get("text", "Text")
277
+ clip_obj = TextClip(content, start=start, duration=duration)
278
+
279
+ style = clip_data.get("style", {})
280
+ clip_obj.set_style(
281
+ font_id=_resolve_font_id(style),
282
+ size=style.get("size"),
283
+ color=style.get("color"),
284
+ alpha=style.get("alpha"),
285
+ bold=style.get("bold"),
286
+ italic=style.get("italic"),
287
+ underline=style.get("underline"),
288
+ )
289
+ clip_obj.set_format(
290
+ align=style.get("align", 1),
291
+ line_spacing=style.get("line_spacing", 0),
292
+ letter_spacing=style.get("letter_spacing", 0),
293
+ line_max_width=style.get("line_max_width", 82),
294
+ typesetting=style.get("typesetting", 0),
295
+ )
296
+
297
+ border = style.get("border")
298
+ if border:
299
+ clip_obj.set_border(width=border.get("width", 0), color=border.get("color", "#000000"))
300
+
301
+ shadow = style.get("shadow")
302
+ if shadow:
303
+ clip_obj.set_shadow(
304
+ alpha=shadow.get("alpha", 0),
305
+ color=shadow.get("color", "#000000"),
306
+ distance=shadow.get("distance", 5.0),
307
+ angle=shadow.get("angle", -45.0),
308
+ diffuse=shadow.get("diffuse", 15),
309
+ )
310
+
311
+ bg = style.get("background")
312
+ if bg:
313
+ clip_obj.set_background(
314
+ color=bg.get("color", "#000000"),
315
+ alpha=bg.get("alpha", 50),
316
+ radius=bg.get("radius", 50),
317
+ width=bg.get("width", 0),
318
+ height=bg.get("height", 0),
319
+ off_x=bg.get("off_x", 50),
320
+ off_y=bg.get("off_y", 50),
321
+ style=bg.get("style", 1),
322
+ )
323
+
324
+ if style.get("curve") is not None:
325
+ clip_obj.set_curve(angle=style["curve"])
326
+
327
+ glow_id = clip_data.get("glow_id") or style.get("glow_id")
328
+ bubble_id = clip_data.get("bubble_id") or style.get("bubble_id")
329
+ if glow_id or bubble_id:
330
+ clip_obj.set_effect(glow_id=glow_id, bubble_id=bubble_id)
331
+
332
+ bloom = clip_data.get("bloom") or style.get("bloom")
333
+ if bloom:
334
+ clip_obj.set_bloom(
335
+ strength=bloom.get("strength", 50),
336
+ range_val=bloom.get("range", 50),
337
+ dir_x=bloom.get("dir_x", 0),
338
+ dir_y=bloom.get("dir_y", 0),
339
+ color=bloom.get("color", ""),
340
+ bloom_type=bloom.get("type", "外发光"),
341
+ )
342
+
343
+ rich_styles = clip_data.get("rich_styles")
344
+ if rich_styles:
345
+ for tag, props in rich_styles.items():
346
+ clip_obj.add_rich_style(tag, **props)
347
+
348
+ # ── D. 贴纸 ─────────────────────────────────────────────────
349
+ elif clip_type == "sticker":
350
+ res_id = clip_data.get("resource_id") or clip_data.get("path")
351
+ if not res_id:
352
+ continue
353
+ clip_obj = StickerClip(res_id, start=start, duration=duration)
354
+
355
+ # ── E. 特效 ─────────────────────────────────────────────────
356
+ elif clip_type == "effect":
357
+ res_id = clip_data.get("resource_id") or clip_data.get("effect_title")
358
+ if not res_id:
359
+ continue
360
+ clip_obj = EffectClip(
361
+ resource_id=res_id,
362
+ start=start,
363
+ duration=duration,
364
+ name=clip_data.get("name", "effect"),
365
+ type=clip_data.get("effect_type", "video_effect"),
366
+ render_index=clip_data.get("render_index"),
367
+ )
368
+ params = clip_data.get("params") or clip_data.get("adjust_params", [])
369
+ if isinstance(params, dict):
370
+ for k, v in params.items():
371
+ clip_obj.set_param(k, v)
372
+ elif isinstance(params, list):
373
+ for p in params:
374
+ clip_obj.set_param(p.get("name"), p.get("value"))
375
+
376
+ # ── F. 滤镜 ─────────────────────────────────────────────────
377
+ elif clip_type == "filter":
378
+ res_id = clip_data.get("resource_id")
379
+ if not res_id:
380
+ continue
381
+ clip_obj = FilterClip(
382
+ resource_id=res_id,
383
+ start=start,
384
+ duration=duration,
385
+ intensity=clip_data.get("intensity", 80),
386
+ name=clip_data.get("name", "filter"),
387
+ )
388
+
389
+ if not clip_obj:
390
+ continue
391
+
392
+ # ── 通用:transform / 动画 / 关键帧 ────────────────────────
393
+ transform = clip_data.get("transform")
394
+ if transform:
395
+ clip_obj.set_transform(
396
+ x=transform.get("x", 0),
397
+ y=transform.get("y", 0),
398
+ scale=transform.get("scale", 100),
399
+ rotation=transform.get("rotation", 0),
400
+ alpha=transform.get("alpha", 100),
401
+ uniform_scale=transform.get("uniform_scale", True),
402
+ scale_x=transform.get("scale_x", 100),
403
+ scale_y=transform.get("scale_y", 100),
404
+ )
405
+
406
+ for anim in clip_data.get("animations", []):
407
+ clip_obj.add_animation(
408
+ anim_type=anim.get("type"),
409
+ resource_id=anim.get("id"),
410
+ duration=_parse_time(anim.get("duration", 500_000)),
411
+ )
412
+
413
+ for kf_group in clip_data.get("keyframes", []):
414
+ prop = kf_group.get("prop")
415
+ for kf_point in kf_group.get("data", []):
416
+ clip_obj.add_keyframe(
417
+ prop,
418
+ _parse_time(kf_point.get("time_offset")),
419
+ kf_point.get("value"),
420
+ )
421
+
422
+ current_track.add(clip_obj)
423
+ if clip_id:
424
+ clip_map[clip_id] = clip_obj
425
+
426
+ # 4. 生成完整剪映草稿 JSON(不写磁盘,直接构建 dict)
427
+ from zcatwx_zil6sm6h_wyaaz._core import Track
428
+
429
+ track_data = [t.export(draft) for t in draft.tracks]
430
+ existing_types = {t.type for t in draft.tracks}
431
+ for default_type in ("video", "audio"):
432
+ if default_type not in existing_types:
433
+ dummy = Track(default_type)
434
+ track_data.insert(0, dummy.export(draft))
435
+
436
+ duration = 0
437
+ for t in draft.tracks:
438
+ if t.segments:
439
+ last = t.segments[-1]
440
+ end = last.start_time + last.duration
441
+ if end > duration:
442
+ duration = end
443
+
444
+ draft_content = {
445
+ "canvas_config": {"width": draft.width, "height": draft.height, "ratio": draft.ratio},
446
+ "color_space": 0,
447
+ "config": {
448
+ "adjust_max_index": 1, "attachment_info": [], "combination_max_index": 1,
449
+ "export_range": None, "extract_audio_last_index": 1,
450
+ "lyrics_recognition_id": "", "lyrics_sync": True, "lyrics_taskinfo": [],
451
+ "maintrack_adsorb": True, "material_save_mode": 0,
452
+ "multi_language_current": "none", "multi_language_list": [],
453
+ "multi_language_main": "none", "multi_language_mode": "none",
454
+ "original_sound_last_index": 1, "record_audio_last_index": 1,
455
+ "sticker_max_index": 1, "subtitle_keywords_config": None,
456
+ "subtitle_recognition_id": "", "subtitle_sync": True, "subtitle_taskinfo": [],
457
+ "system_font_list": [], "video_mute": False, "zoom_info_params": None,
458
+ },
459
+ "cover": None, "create_time": 0, "duration": duration,
460
+ "extra_info": None, "fps": 30.0, "free_render_index_mode_on": False,
461
+ "group_container": None, "id": draft.project_id,
462
+ "keyframe_graph_list": [],
463
+ "keyframes": {
464
+ "adjusts": [], "audios": [], "effects": [], "filters": [],
465
+ "handwrites": [], "stickers": [], "texts": [], "videos": [],
466
+ },
467
+ "materials": draft.materials,
468
+ "mutable_config": None, "name": save_name, "new_version": "110.0.0",
469
+ "platform": {
470
+ "app_id": 3704, "app_source": "lv", "app_version": "5.9.0",
471
+ "device_id": "", "hard_disk_id": "", "mac_address": "",
472
+ "os": "mac", "os_version": "14.0",
473
+ },
474
+ "last_modified_platform": {
475
+ "app_id": 3704, "app_source": "lv", "app_version": "5.9.0",
476
+ "device_id": "", "hard_disk_id": "", "mac_address": "",
477
+ "os": "mac", "os_version": "14.0",
478
+ },
479
+ "relationships": [], "render_index_track_mode_on": True,
480
+ "retouch_cover": None, "source": "default",
481
+ "static_cover_image_path": "", "tracks": track_data,
482
+ "update_time": 0, "version": 360000,
483
+ }
484
+
485
+ draft_json_str = json.dumps(draft_content, ensure_ascii=False)
486
+
487
+ # 5. 加密后返回
488
+ return DraftCrypto.encrypt(draft_json_str)