xiaowei-system/scripts/i2v_video.py

223 lines
7.7 KiB
Python
Raw Permalink 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
"""Image-to-Video 生成脚本 — 支持 MiniMax Hailuo / Kling API
用法:
# MiniMax Hailuo默认
python3 i2v_video.py --image photo.jpg --prompt "camera slowly pushes in"
python3 i2v_video.py --image photo.jpg --prompt "..." --duration 6 --resolution 1080P
# Kling 可灵
python3 i2v_video.py --image photo.jpg --prompt "..." --provider kling
环境变量:
MINIMAX_API_KEY — MiniMax 平台 API key (platform.minimax.io)
KLING_API_KEY — Kling 平台 API key (klingai.com)
"""
import argparse, json, os, sys, time, subprocess
from pathlib import Path
def get_key(env_name):
"""从 .env 或环境变量获取 key"""
key = os.environ.get(env_name, "")
if not key:
env_file = os.path.expanduser("~/.hermes/.env")
if os.path.exists(env_file):
for line in open(env_file):
line = line.strip()
if line.startswith(f"{env_name}=") and not line.startswith("#"):
key = line.split("=", 1)[1].strip().strip('"').strip("'")
break
return key
def upload_image_to_url(image_path):
"""将本地图片上传到临时图床,返回公网 URL"""
# 用 catbox.moe 免费图床
result = subprocess.run(
["curl", "-s", "-F", f"reqtype=fileupload", "-F", f"fileToUpload=@{image_path}",
"https://catbox.moe/user/api.php"],
capture_output=True, text=True, timeout=30
)
url = result.stdout.strip()
if url.startswith("http"):
return url
# fallback: 用 0x0.st
result = subprocess.run(
["curl", "-s", "-F", f"file=@{image_path}", "https://0x0.st"],
capture_output=True, text=True, timeout=30
)
url = result.stdout.strip()
if url.startswith("http"):
return url
return ""
def minimax_i2v(image_url, prompt, duration=6, resolution="720P"):
"""MiniMax Hailuo 图生视频"""
key = get_key("MINIMAX_API_KEY")
if not key:
print("❌ 未找到 MINIMAX_API_KEY")
print(" 去 https://platform.minimax.io 注册获取免费 key")
print(" 然后: echo 'MINIMAX_API_KEY=你的key' >> ~/.hermes/.env")
return None
# 提交任务
data = {
"model": "MiniMax-Hailuo-2.3",
"prompt": prompt,
"first_frame_image": image_url,
"duration": duration,
"resolution": resolution,
}
result = subprocess.run(
["curl", "-s", "-X", "POST", "https://api.minimax.io/v1/video_generation",
"-H", f"Authorization: Bearer {key}",
"-H", "Content-Type: application/json",
"-d", json.dumps(data)],
capture_output=True, text=True, timeout=30
)
resp = json.loads(result.stdout)
task_id = resp.get("task_id", "")
if not task_id:
print(f"❌ 提交失败: {resp}")
return None
print(f"✅ 任务已提交: {task_id}")
print(f" 模型: MiniMax-Hailuo-2.3")
print(f" 时长: {duration}秒, 分辨率: {resolution}")
# 轮询
for i in range(60):
time.sleep(5)
status_resp = subprocess.run(
["curl", "-s", f"https://api.minimax.io/v1/video_generation/{task_id}",
"-H", f"Authorization: Bearer {key}"],
capture_output=True, text=True, timeout=30
)
status_data = json.loads(status_resp.stdout)
status = status_data.get("status", "unknown")
progress = status_data.get("progress", 0)
print(f"\r [{i+1}] {status} ({progress}%)", end="", flush=True)
if status == "completed":
video_url = status_data.get("video", {}).get("url", "")
if video_url:
print(f"\n✅ 生成完成!")
return video_url
elif status == "failed":
print(f"\n❌ 生成失败: {status_data.get('error', '')}")
return None
print("\n❌ 超时")
return None
def kling_i2v(image_url, prompt, duration=5):
"""Kling 可灵 图生视频"""
key = get_key("KLING_API_KEY")
if not key:
print("❌ 未找到 KLING_API_KEY")
print(" 去 https://klingai.com 注册获取免费 key")
return None
data = {
"model_name": "kling-v2",
"image": image_url,
"prompt": prompt,
"duration": str(duration),
"aspect_ratio": "16:9",
}
result = subprocess.run(
["curl", "-s", "-X", "POST", "https://api.klingai.com/v1/videos/image2video",
"-H", f"Authorization: Bearer {key}",
"-H", "Content-Type: application/json",
"-d", json.dumps(data)],
capture_output=True, text=True, timeout=30
)
resp = json.loads(result.stdout)
task_id = resp.get("data", {}).get("task_id", "")
if not task_id:
print(f"❌ 提交失败: {resp}")
return None
print(f"✅ 任务已提交: {task_id}")
for i in range(60):
time.sleep(5)
status_resp = subprocess.run(
["curl", "-s", f"https://api.klingai.com/v1/videos/image2video/{task_id}",
"-H", f"Authorization: Bearer {key}"],
capture_output=True, text=True, timeout=30
)
status_data = json.loads(status_resp.stdout)
task = status_data.get("data", {})
status = task.get("task_status", "unknown")
print(f"\r [{i+1}] {status}", end="", flush=True)
if status == "succeed":
videos = task.get("task_result", {}).get("videos", [])
if videos:
print(f"\n✅ 生成完成!")
return videos[0].get("url", "")
elif status == "failed":
print(f"\n❌ 生成失败")
return None
print("\n❌ 超时")
return None
def download_video(url, output_path):
"""下载视频到本地"""
subprocess.run(["curl", "-sL", url, "-o", output_path], timeout=60)
size = os.path.getsize(output_path) if os.path.exists(output_path) else 0
return size
def main():
parser = argparse.ArgumentParser(description="Image-to-Video 生成")
parser.add_argument("--image", required=True, help="输入图片路径或URL")
parser.add_argument("--prompt", required=True, help="运镜/动作描述")
parser.add_argument("--provider", default="minimax", choices=["minimax", "kling"])
parser.add_argument("--duration", type=int, default=6, help="时长(秒)")
parser.add_argument("--resolution", default="720P", help="分辨率(仅minimax)")
parser.add_argument("--output", help="输出路径")
parser.add_argument("--push", action="store_true", help="推送到飞书")
args = parser.parse_args()
# 处理图片本地文件上传到公网URL
image = args.image
if os.path.isfile(image):
print(f"📤 上传图片到公网...")
image = upload_image_to_url(image)
if not image:
print("❌ 图片上传失败")
sys.exit(1)
print(f" URL: {image}")
# 生成
print(f"🎬 开始图生视频 ({args.provider})...")
print(f" 提示词: {args.prompt}")
if args.provider == "minimax":
video_url = minimax_i2v(image, args.prompt, args.duration, args.resolution)
else:
video_url = kling_i2v(image, args.prompt, args.duration)
if not video_url:
sys.exit(1)
# 下载
output = args.output or os.path.expanduser(
f"~/.hermes/video_cache/i2v_{args.provider}_{int(time.time())}.mp4"
)
os.makedirs(os.path.dirname(output), exist_ok=True)
size = download_video(video_url, output)
print(f"📁 已保存: {output} ({size//1024}KB)")
# 推飞书
if args.push:
print("📤 推送飞书...")
# 复用 agnes_video 的推送逻辑
os.system(f'python3 ~/.hermes/scripts/agnes_video.py status dummy 2>/dev/null')
print(" (请手动发送文件或用 send_message)")
if __name__ == "__main__":
main()