cang-ying/video-ai-system/tools/local_motion.py
Zhuyuan Operations ce32073d07 D205 面板功能大升级+本地漫剧管线+经验自动机制+飞书资料归档
- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布
- 面板: 经验自动检索注入+存经验按钮(experience.py)
- 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1
- 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录
- 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
2026-08-03 22:55:02 +08:00

71 lines
3.0 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
"""local_motion.py — 本地运镜工具BC-006 IMAGE-FIRST 默认路由 LOCAL_MOTION
静态图 → ffmpeg zoompan → 竖屏 9:16 运镜片段¥0 视频模型费)
用法:
python tools/local_motion.py <图片文件夹> -o <输出文件夹> [--dur 6] [--fps 24]
运镜模式循环: push_in / zoom_out / pan_left / pan_right / pan_up / static
输出: <输出文件夹>/shot_01.mp4 ... 每个片段 dur 秒
"""
import os, sys, subprocess, argparse, glob
def normalize(fps):
return ["-vf", f"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=4320:7680,fps={fps}"]
def zoompan_filter(mode, frames, fps):
z = "z='min(1+0.0008*on,1.6)'" # push_in 推近
x = "x='iw/2-(iw/zoom/2)'"
y = "y='ih/2-(ih/zoom/2)'"
if mode == "zoom_out":
z = "z='max(1.5-0.0008*on,1.0)'"
elif mode == "pan_left": # 从左扫到右
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(on/{})'".format(frames)
elif mode == "pan_right": # 从右扫到左
z = "z='1.2'"; x = "x='(iw-iw/zoom)*(1-on/{})'".format(frames)
elif mode == "pan_up": # 从下往上
z = "z='1.2'"; y = "y='(ih-ih/zoom)*(1-on/{})'".format(frames)
elif mode == "static":
z = "z='1.0'"
return f"zoompan={z}:{x}:{y}:d={frames}:s=1080x1920:fps={fps}"
def make_shot(img, out, dur, fps, mode):
frames = dur * fps
vf = normalize(fps)[1] + "," + zoompan_filter(mode, frames, fps)
cmd = ["ffmpeg", "-y", "-loop", "1", "-i", img, "-vf", vf,
"-t", str(dur), "-r", str(fps), "-c:v", "libx264", "-preset", "fast",
"-crf", "20", "-pix_fmt", "yuv420p", "-an", out]
r = subprocess.run(cmd, capture_output=True, text=True)
return r.returncode == 0, r.stderr[-200:] if r.returncode else ""
def main():
ap = argparse.ArgumentParser()
ap.add_argument("folder")
ap.add_argument("-o", "--output", default="local_motion_out")
ap.add_argument("--dur", type=int, default=6)
ap.add_argument("--fps", type=int, default=24)
args = ap.parse_args()
imgs = sorted(glob.glob(os.path.join(args.folder, "*.png")) +
glob.glob(os.path.join(args.folder, "*.jpg")) +
glob.glob(os.path.join(args.folder, "*.jpeg")) +
glob.glob(os.path.join(args.folder, "*.webp")))
if not imgs:
print("❌ 文件夹里没有图片"); return 1
os.makedirs(args.output, exist_ok=True)
modes = ["push_in", "zoom_out", "pan_left", "pan_right", "pan_up", "static"]
ok = 0
for i, img in enumerate(imgs[:64], 1):
mode = modes[(i - 1) % len(modes)]
out = os.path.join(args.output, f"shot_{i:02d}.mp4")
print(f"[{i}/{len(imgs)}] {os.path.basename(img)} -> {mode} ...", end=" ")
good, err = make_shot(img, out, args.dur, args.fps, mode)
if good:
print(""); ok += 1
else:
print("", err[-120:])
print(f"完成: {ok}/{len(imgs)} 个片段已生成 -> {args.output}")
if __name__ == "__main__":
sys.exit(main())