cang-ying/engines/multi-reference-video-adapter.py

423 lines
16 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MULTI-REFERENCE-VIDEO-ADAPTER
多参考图视频适配器 支持苏白+牌匾+场景多参考输入
功能:
1. 检查视频API是否支持多参考图输入
2. 如果支持: 封装多参考图接口统一调用
3. 如果不支持: 明确报错回退到"单参考图+后期合成"路线
4. 提供统一的调用接口给上游 Agent
用法:
python multi-reference-video-adapter.py --prompt "苏白站在天道宗牌匾下" \\
--references char-003-subai.png tdz-plaque.png env-baizonghui.png \\
--output output.mp4
检查API能力:
python multi-reference-video-adapter.py --check-api
"""
import os
import sys
import json
import argparse
import requests
from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "engines"))
def load_api_config():
"""从苍耳本机仓库外密钥文件和环境变量加载配置"""
config = {}
env_files = [
Path(os.environ["VIDEO_AI_SECRETS_FILE"]) if os.environ.get("VIDEO_AI_SECRETS_FILE") else None,
Path("/root/guanghulab-local-secrets/cang-ying.env"),
Path("~/guanghulab-local-secrets/cang-ying.env").expanduser(),
Path("/Users/bingshuolingdianyuanhe/Documents/guanghulab-local-secrets/video-ai-system.env"),
]
for env_file in env_files:
if env_file is None:
continue
if not env_file.exists():
continue
with open(env_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
key, _, val = line.partition("=")
config[key.strip()] = val.strip().strip('"').strip("'")
for key in ["JIMENG_API_KEY", "JIMENG_BASE_URL", "JIMENG_MODEL"]:
if os.environ.get(key):
config[key] = os.environ[key]
return config
class MultiReferenceVideoAdapter:
"""多参考图视频适配器"""
def __init__(self, live_capability_check=False, allow_single_reference_fallback=False):
self.config = load_api_config()
self.api_key = self.config.get("JIMENG_API_KEY", "")
self.base_url = self.config.get("JIMENG_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
self.model = self.config.get("JIMENG_MODEL", "doubao-seedance-2-0-260128")
self.live_capability_check = live_capability_check
self.allow_single_reference_fallback = allow_single_reference_fallback
# API 能力探测结果缓存
self._api_capabilities = None
def check_api_capabilities(self, live=None):
"""
检查 API 是否支持多参考图
返回: {
"multi_reference_supported": bool,
"max_references": int,
"supported_types": list, # ["image_url", "image_url_2", ...]
"details": str
}
"""
if self._api_capabilities:
return self._api_capabilities
live = self.live_capability_check if live is None else live
print("🔍 检查 Seedance API 多参考图支持...")
# 根据 Volcengine 官方文档 (https://www.volcengine.com/docs/82379/1520757)
# Seedance 2.0 API 的 content 数组支持多个 image_url 对象
# 但需要实际测试确认
# 理论上的 API 结构:
# content: [
# { type: "text", text: "..." },
# { type: "image_url", image_url: { url: "data:image/png;base64,..." } },
# { type: "image_url", image_url: { url: "data:image/png;base64,..." } }, # 第二张参考图
# ]
if not live:
test_result = {
"multi_reference_supported": False,
"max_references": 1,
"supported_types": ["image_url"],
"status": "unverified",
"details": "未执行付费/联网实测;需要 --live-check 才会提交真实 API 探测任务",
"requires_live_check": True
}
else:
test_result = self._test_multi_reference()
self._api_capabilities = test_result
return test_result
def _test_multi_reference(self):
"""
实际测试 API 是否支持多参考图
方法: 提交一个测试请求包含2张参考图观察响应
"""
if not self.api_key:
return {
"multi_reference_supported": False,
"max_references": 1,
"supported_types": ["image_url"],
"details": "未找到 JIMENG_API_KEY无法执行 live check",
"status": "missing_api_key"
}
# 构造一个最小测试请求
test_prompt = "test multi-reference support"
# 创建1x1像素的测试图片 (PNG)
import base64
from io import BytesIO
try:
from PIL import Image
img = Image.new("RGB", (32, 32), color=(255, 0, 0))
buf = BytesIO()
img.save(buf, format="PNG")
test_img_b64 = base64.b64encode(buf.getvalue()).decode()
except ImportError:
# 如果没有 PIL用空base64
test_img_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWg"
# 构造 content 数组 (2张参考图)
content = [
{"type": "text", "text": test_prompt},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{test_img_b64}"}},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{test_img_b64}"}},
]
payload = {
"model": self.model,
"content": content,
"duration": 4, # 最短时长,省钱
"resolution": "480p"
}
# 发送请求
try:
print(" 📤 发送测试请求 (2张参考图)...")
url = f"{self.base_url}/contents/generations/tasks"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
# 成功API 支持多参考图
print(" ✅ API 支持多参考图!")
return {
"multi_reference_supported": True,
"max_references": 2, # 需要逐步测试确定上限
"supported_types": ["image_url"],
"details": "API 成功接受2张参考图"
}
elif response.status_code == 400:
# 看错误信息
error_data = response.json()
error_msg = error_data.get("error", {}).get("message", "")
print(f" ❌ API 不支持多参考图: {error_msg}")
return {
"multi_reference_supported": False,
"max_references": 1,
"supported_types": ["image_url"], # 只支持单张
"details": error_msg,
"error_response": error_data
}
else:
print(f" ⚠️ 未知响应: {response.status_code}")
return {
"multi_reference_supported": False,
"max_references": 1,
"details": f"Unknown response: {response.status_code}"
}
except Exception as e:
print(f" ❌ 测试失败: {e}")
return {
"multi_reference_supported": False,
"max_references": 1,
"details": f"Test failed: {e}"
}
def generate_video(self, prompt, reference_images, output_path=None, duration=5, resolution="720p"):
"""
生成视频 (多参考图)
参数:
prompt: str - 提示词
reference_images: list[str] - 参考图路径列表
output_path: str - 输出路径
duration: int - 时长 (4-15)
resolution: str - 分辨率 ("480p" | "720p")
返回:
{
"success": bool,
"task_id": str,
"output_path": str,
"method": str, # "multi-reference" | "single-reference+composite"
"warning": str
}
"""
print(f"\n🎬 生成视频 (多参考图)")
print(f" 提示词: {prompt[:60]}...")
print(f" 参考图数量: {len(reference_images)}")
for i, img in enumerate(reference_images):
print(f" [{i+1}] {Path(img).name}")
# 检查 API 能力
capabilities = self.check_api_capabilities()
if capabilities["multi_reference_supported"]:
# API 支持多参考图 → 直接调用
print(f"\n ✅ API 支持多参考图,直接调用...")
result = self._generate_multi_reference(prompt, reference_images, output_path, duration, resolution)
result["method"] = "multi-reference"
return result
else:
# API 不支持多参考图 → 明确报错 + 建议回退方案
print(f"\n ❌ API 不支持多参考图")
print(f" 📋 错误详情: {capabilities['details']}")
print(f"\n 💡 回退方案:")
print(f" 1. 使用第一张参考图 (苏白) 生成视频")
print(f" 2. 后期合成牌匾/场景 (平面追踪 + 贴图)")
print(f" 3. 或使用可灵生成角色Seedance 生成场景,后期合成")
if not self.allow_single_reference_fallback:
return {
"success": False,
"method": "not-run",
"error": "多参考图能力未确认或不支持,未自动回退单参考图以避免误导一致性测试",
"capabilities": capabilities,
"next_step": "使用 --live-check 验证多参考图,或显式传入 --allow-single-reference-fallback 做调试占位"
}
# 回退: 只用第一张参考图。必须显式开启,避免误导牌匾/场景一致性判断。
warning = "API不支持多参考图已回退到单参考图模式。牌匾/场景一致性需要后期合成。"
print(f"\n 🔄 回退: 使用第一张参考图生成...")
result = self._generate_single_reference(prompt, reference_images[0], output_path, duration, resolution)
result["method"] = "single-reference+composite"
result["warning"] = warning
result["fallback_reason"] = capabilities["details"]
return result
def _generate_multi_reference(self, prompt, reference_images, output_path, duration, resolution):
"""调用多参考图 API"""
# 构造 content 数组
content = [{"type": "text", "text": prompt}]
for img_path in reference_images:
img_path = Path(img_path)
if not img_path.exists():
print(f" ⚠️ 参考图不存在: {img_path}")
continue
# 读取图片并转 base64
import base64
with open(img_path, "rb") as f:
img_data = f.read()
b64 = base64.b64encode(img_data).decode()
mime = "image/png" if img_path.suffix.lower() == ".png" else "image/jpeg"
content.append({
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{b64}"}
})
# 调用 API
payload = {
"model": self.model,
"content": content,
"duration": duration,
"resolution": resolution
}
print(f" 📤 提交任务...")
url = f"{self.base_url}/contents/generations/tasks"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers, timeout=60)
response.raise_for_status()
data = response.json()
task_id = data.get("id") or data.get("task_id")
print(f" ✅ 任务已提交: {task_id}")
# 返回任务ID等待轮询
return {
"success": True,
"task_id": task_id,
"output_path": output_path,
"api_response": data
}
def _generate_single_reference(self, prompt, reference_image, output_path, duration, resolution):
"""回退: 单参考图生成"""
# 调用现有的 video-api-adapter (Node.js)
# 这里用 subprocess 调用
import subprocess
print(f" 📤 调用单参考图 API...")
# 构造调用参数
node_script = PROJECT_ROOT / "engines" / "video-api-adapter.js"
cmd = [
"node", str(node_script),
"--prompt", prompt,
"--reference-image", str(reference_image),
"--duration", str(duration),
"--resolution", resolution
]
# 执行
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f" ❌ 调用失败: {result.stderr}")
return {"success": False, "error": result.stderr}
print(f" ✅ 任务已提交")
return {"success": True, "method": "single-reference", "stdout": result.stdout}
def batch_generate(self, shots_config):
"""
批量生成 (从配置文件)
shots_config 格式:
[
{
"shot_id": "E1-SHOT01",
"prompt": "...",
"references": ["char.png", "prop.png", "env.png"],
"output": "output/E1-SHOT01.mp4"
},
...
]
"""
results = []
for shot in shots_config:
result = self.generate_video(
prompt=shot["prompt"],
reference_images=shot["references"],
output_path=shot["output"]
)
results.append(result)
return results
def main():
parser = argparse.ArgumentParser(description="MULTI-REFERENCE-VIDEO-ADAPTER")
parser.add_argument("--check-api", action="store_true", help="检查API多参考图支持")
parser.add_argument("--live-check", action="store_true",
help="执行真实 API 探测请求,可能产生费用")
parser.add_argument("--allow-single-reference-fallback", action="store_true",
help="多参考图不可用时允许单参考图调试占位")
parser.add_argument("--prompt", type=str, help="提示词")
parser.add_argument("--references", type=str, nargs="+", help="参考图路径列表")
parser.add_argument("--output", type=str, help="输出路径")
parser.add_argument("--duration", type=int, default=5, help="时长 (4-15)")
parser.add_argument("--resolution", type=str, default="720p", choices=["480p", "720p"], help="分辨率")
args = parser.parse_args()
adapter = MultiReferenceVideoAdapter(
live_capability_check=args.live_check,
allow_single_reference_fallback=args.allow_single_reference_fallback
)
if args.check_api:
capabilities = adapter.check_api_capabilities(live=args.live_check)
print(f"\n📊 API 能力报告:")
print(f" 多参考图支持: {capabilities['multi_reference_supported']}")
print(f" 最大参考图数: {capabilities['max_references']}")
print(f" 详情: {capabilities['details']}")
return
if not args.prompt or not args.references:
parser.print_help()
return
result = adapter.generate_video(
prompt=args.prompt,
reference_images=args.references,
output_path=args.output,
duration=args.duration,
resolution=args.resolution
)
print(f"\n📋 生成结果:")
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()