You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
7.4 KiB
242 lines
7.4 KiB
#!/usr/bin/env python3
|
|
"""
|
|
剪映/CapCut 导出脚本(hailuo-promo 专用封装)
|
|
|
|
封装 clip-export skill 的 jy_wrapper API,预置 Hailuo Promo 专用配置:
|
|
- 竖屏 1080×1920 分辨率
|
|
- 数字人 PiP 左下角定位
|
|
- 字幕轨道
|
|
- BGM 混合
|
|
|
|
使用方式:
|
|
python export_to_jianying.py --project-name "my_promo" \\
|
|
--photos photo_01.jpg photo_02.jpg photo_03.jpg \\
|
|
--digital-person talking_head_green.mp4 \\
|
|
--voiceover voiceover.mp3 \\
|
|
--bgm bgm.mp3 \\
|
|
--subtitles voiceover_subtitle.json \\
|
|
--output 导出到剪映草稿目录
|
|
|
|
依赖:
|
|
pip3 install pyJianYingDraft
|
|
需安装 clip-export skill(脚本通过相对路径引用 jy_wrapper)
|
|
|
|
脚本路径:
|
|
../clip-export/scripts/jy_wrapper.py (相对于本脚本)
|
|
或设置 JY_WRAPPER_PATH 环境变量覆盖
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
# ─────────────────────────── 路径配置 ───────────────────────────
|
|
|
|
# 尝试多个可能路径找到 jy_wrapper
|
|
SEARCH_PATHS = [
|
|
Path(__file__).parent.parent / "clip-export" / "scripts",
|
|
Path.home() / ".claude" / "skills" / "clip-export" / "scripts",
|
|
Path("/Users/ilike/Movies/Hub/skills/clip-export/scripts"),
|
|
]
|
|
|
|
JY_WRAPPER_PATH = os.environ.get("JY_WRAPPER_PATH")
|
|
if JY_WRAPPER_PATH:
|
|
sys.path.insert(0, JY_WRAPPER_PATH)
|
|
else:
|
|
for p in SEARCH_PATHS:
|
|
if (p / "jy_wrapper.py").exists():
|
|
sys.path.insert(0, str(p))
|
|
break
|
|
else:
|
|
print("❌ 找不到 jy_wrapper.py,请确保:")
|
|
print(" 1. 安装了 clip-export skill")
|
|
print(" 2. 或设置 JY_WRAPPER_PATH 环境变量指向 clip-export/scripts/ 目录")
|
|
sys.exit(1)
|
|
|
|
from jy_wrapper import JyProject, ClipSettings
|
|
|
|
|
|
# ─────────────────────────── 配置常量 ───────────────────────────
|
|
|
|
CANVAS_W = 1080
|
|
CANVAS_H = 1920
|
|
|
|
# 数字人 PiP 参数(Hailuo Promo 标准位置)
|
|
PIP_SCALE_X = 0.4
|
|
PIP_SCALE_Y = 0.4
|
|
PIP_POS_X = -0.8 # 左下角(transform_x 范围 -1 到 1)
|
|
PIP_POS_Y = 0.5
|
|
|
|
# 字幕位置(transform_y 范围 -1 到 1,底部约 -0.35)
|
|
SUBTITLE_TRANSFORM_Y = -0.35
|
|
|
|
# BGM 音量
|
|
BGM_VOLUME = 0.15
|
|
|
|
|
|
# ─────────────────────────── 核心函数 ───────────────────────────
|
|
|
|
|
|
def add_photo_carousel(
|
|
project: JyProject,
|
|
photos: List[str],
|
|
each_duration: float = 3.0,
|
|
transition: str = "slideleft",
|
|
track_name: str = "主视频",
|
|
) -> None:
|
|
"""添加写真图轮播(带转场)"""
|
|
for i, photo in enumerate(photos):
|
|
start_time = i * each_duration
|
|
project.add_video(
|
|
photo,
|
|
start_time=start_time,
|
|
duration=f"{each_duration}s",
|
|
track_name=track_name,
|
|
)
|
|
|
|
|
|
def add_digital_person_pip(
|
|
project: JyProject,
|
|
video_path: str,
|
|
start_time: float = 0.5,
|
|
duration: float = 15.0,
|
|
track_name: str = "数字人PiP",
|
|
) -> None:
|
|
"""添加数字人 PiP(预置 Hailuo Promo 标准位置)"""
|
|
pip_settings = ClipSettings(
|
|
scale_x=PIP_SCALE_X,
|
|
scale_y=PIP_SCALE_Y,
|
|
transform_x=PIP_POS_X,
|
|
transform_y=PIP_POS_Y,
|
|
)
|
|
project.add_video(
|
|
video_path,
|
|
start_time=start_time,
|
|
duration=f"{duration}s",
|
|
track_name=track_name,
|
|
clip_settings=pip_settings,
|
|
)
|
|
|
|
|
|
def add_bgm(project: JyProject, bgm_path: str, start_time: float = 0.0) -> None:
|
|
"""添加 BGM(预置音量 0.15)"""
|
|
project.add_audio(
|
|
bgm_path, start_time=start_time, volume=BGM_VOLUME, track_name="BGM"
|
|
)
|
|
|
|
|
|
def add_voiceover(
|
|
project: JyProject,
|
|
voiceover_path: str,
|
|
start_time: float = 0.0,
|
|
duration: Optional[float] = None,
|
|
track_name: str = "配音",
|
|
) -> None:
|
|
"""添加配音"""
|
|
kwargs = {"start_time": start_time, "track_name": track_name, "volume": 1.0}
|
|
if duration:
|
|
kwargs["duration"] = f"{duration}s"
|
|
project.add_audio(voiceover_path, **kwargs)
|
|
|
|
|
|
# ─────────────────────────── 主入口 ───────────────────────────
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="导出 Hailuo Promo 项目到剪映/CapCut(预置 1080×1920 + PiP 定位)"
|
|
)
|
|
parser.add_argument(
|
|
"--project-name", required=True, help="项目名称(显示在剪映中)"
|
|
)
|
|
parser.add_argument("--photos", nargs="+", required=True, help="写真图路径列表")
|
|
parser.add_argument("--digital-person", default=None, help="数字人视频路径(可选)")
|
|
parser.add_argument("--voiceover", default=None, help="配音路径(可选)")
|
|
parser.add_argument("--bgm", default=None, help="BGM 路径(可选)")
|
|
parser.add_argument(
|
|
"--subtitles",
|
|
default=None,
|
|
help="字幕 JSON 文件(由 audio_transcribe_lyrics 生成,可选)",
|
|
)
|
|
parser.add_argument(
|
|
"--each-duration",
|
|
type=float,
|
|
default=3.0,
|
|
help="每张写真展示时长(秒,默认 3.0)",
|
|
)
|
|
parser.add_argument(
|
|
"--pip-start",
|
|
type=float,
|
|
default=0.5,
|
|
help="数字人 PiP 出现时间(秒,默认 0.5)",
|
|
)
|
|
parser.add_argument(
|
|
"--app-source",
|
|
default=None,
|
|
choices=["lv", "cc"],
|
|
help="目标软件(lv=剪映,cc=CapCut;不指定则询问)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# 创建项目(竖屏 1080×1920)
|
|
app_source = args.app_source
|
|
if not app_source:
|
|
print("\n🎯 目标平台选择:")
|
|
print(" [1] 剪映(中文版)")
|
|
print(" [2] CapCut(国际版)")
|
|
choice = input("请选择 [1/2]: ").strip()
|
|
app_source = "lv" if choice == "1" else "cc"
|
|
|
|
project = JyProject(
|
|
args.project_name,
|
|
width=CANVAS_W,
|
|
height=CANVAS_H,
|
|
direct=True,
|
|
overwrite=True,
|
|
app_source=app_source,
|
|
)
|
|
|
|
# 添加写真轮播
|
|
print(f"📷 添加 {len(args.photos)} 张写真...")
|
|
add_photo_carousel(
|
|
project,
|
|
args.photos,
|
|
each_duration=args.each_duration,
|
|
)
|
|
|
|
# 添加数字人 PiP
|
|
if args.digital_person:
|
|
print(f"🤳 添加数字人 PiP...")
|
|
add_digital_person_pip(project, args.digital_person, start_time=args.pip_start)
|
|
|
|
# 添加配音
|
|
if args.voiceover:
|
|
print(f"🎙️ 添加配音...")
|
|
add_voiceover(project, args.voiceover)
|
|
|
|
# 添加 BGM
|
|
if args.bgm:
|
|
print(f"🎵 添加 BGM...")
|
|
add_bgm(project, args.bgm)
|
|
|
|
# 添加字幕(如果提供 JSON)
|
|
if args.subtitles:
|
|
print(f"📝 导入字幕...")
|
|
try:
|
|
project.import_srt(args.subtitles, track_name="字幕")
|
|
except Exception:
|
|
print(f"⚠️ 字幕导入失败(可能格式不兼容),跳过")
|
|
|
|
# 保存
|
|
print("\n💾 保存到剪映/CapCut 草稿目录...")
|
|
project.save()
|
|
print(
|
|
f"✅ 完成!请打开{'剪映' if app_source == 'lv' else 'CapCut'}查看项目「{args.project_name}」"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|