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

423 lines
16 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
# -*- 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()