Files
Popiai-skill/social-media/hilo-promo/scripts/export_to_jianying.py
T

243 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()