Popiai-skill仓库
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

#!/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()