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