Files

271 lines
11 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.
import os
import json
import traceback
import time
import requests
import random
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from urllib3.util.ssl_ import create_urllib3_context
from .constants import get_project_category, get_project_name, project_root, comfy_root
from PIL import Image
import PIL
import base64
import io
import folder_paths
import numpy as np
import torch
import ssl
import hashlib
from modelscope.hub.api import HubApi
from modelscope.utils.constant import DEFAULT_MODEL_REVISION
from pathlib import Path
# API URL for DashScope
dashscope_api_url = 'https://dashscope.aliyuncs.com'
# API URL for file upload
file_upload_api_url = 'https://api.modelscope.cn'
# Output folder for saving videos
output_folder = os.path.join(comfy_root, 'output')
os.makedirs(output_folder, exist_ok=True)
# Config file path
config_file = os.path.join(project_root, 'config.json')
# Try to load API key from config file
DASHSCOPE_API_KEY = None
try:
if os.path.exists(config_file):
with open(config_file, 'r') as file:
data = file.read()
json_data = json.loads(data)
DASHSCOPE_API_KEY = json_data.get("DASHSCOPE_API_KEY")
except Exception as e:
print(f"Error loading config file: {e}")
NODE_CATEGORY = get_project_category("wanapi")
# 设置ModelScope缓存目录
os.environ['MODELSCOPE_CACHE'] = os.path.join(comfy_root, 'modelscope_cache')
# 添加重试配置
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[500, 502, 503, 504]
)
http_adapter = HTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.mount("http://", http_adapter)
session.mount("https://", http_adapter)
class WanAPIImageToVideo:
"""WanAPI图片转视频节点"""
NAME = get_project_name('WanAPI_Image2Video') # 添加节点名称
CATEGORY = NODE_CATEGORY
RETURN_TYPES = ("STRING", "STRING",) # 添加历史记录输出
RETURN_NAMES = ("video_url", "history_info",) # 添加历史记录名称
FUNCTION = "generate_video"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_url": ("STRING", {"default": ""}),
"model": (["wanx2.1-i2v-plus", "wanx2.1-i2v-turbo"], {
"default": "wanx2.1-i2v-plus",
"description": "plus: 高质量(5秒), turbo: 快速生成(3-5秒)"
}),
"duration": ([3, 4, 5], { # 改为下拉选择
"default": 5,
"description": "plus模型固定5秒,turbo模型可选3-5秒"
}),
},
"optional": {
"api_key": ("STRING", {"default": ""}),
"prompt": ("STRING", {
"default": "",
"multiline": True,
"description": "生成视频的文本提示词"
}),
"seed": ("INT", {
"default": -1,
"min": -1,
"max": 2147483647,
"description": "-1表示随机种子"
}),
"prompt_extend": (["True", "False"], {
"default": "True",
"description": "是否启用提示词智能扩展"
})
}
}
# 删除以下重复定义:
# RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("video_url",)
# FUNCTION = "generate_video"
# CATEGORY = "WanAPI"
def __init__(self):
self.config = self._load_config()
print(f"✅ WanAPI视频节点初始化成功,API Key: {'已配置' if self.config.get('DASHSCOPE_API_KEY') else '未配置'}")
def _load_config(self):
"""加载配置文件"""
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
print(f"📝 加载配置:{config_path}")
return config
except Exception as e:
print(f"⚠️ 配置文件加载失败:{str(e)}")
return {}
def generate_video(self, image_url, model="wanx2.1-i2v-plus", duration=5, api_key="", prompt="", seed=-1, prompt_extend="True"):
"""生成视频并等待完成"""
try:
if not image_url:
raise ValueError("无效图片URL,请先连接上传节点")
# 验证模型和时长组合
if model == "wanx2.1-i2v-plus" and duration != 5:
print("⚠️ wanx2.1-i2v-plus 模型仅支持5秒视频,已自动调整")
duration = 5
elif model == "wanx2.1-i2v-turbo" and duration not in [3, 4, 5]:
print(f"⚠️ wanx2.1-i2v-turbo 模型仅支持3、4、5秒视频,已自动调整为5秒")
duration = 5
# 优先使用参数中的API密钥,其次使用配置文件中的密钥
api_key = api_key or self.config.get("DASHSCOPE_API_KEY", "")
print(f"""
🎬 开始生成视频:
- 模型:{model}
- 时长:{duration}秒
- 提示词:{prompt if prompt else '无'}
- 提示词扩展:{'开启' if prompt_extend == 'True' else '关闭'}
- 随机种子:{seed if seed != -1 else '随机'}
""")
# 调用WanAPI创建任务
response = requests.post(
"https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-DashScope-Async": "enable"
},
json={
"model": model,
"input": {
"prompt": prompt,
"img_url": image_url
},
"parameters": {
"duration": duration,
"seed": seed if seed != -1 else random.randint(0, 2147483647),
"prompt_extend": prompt_extend == "True"
}
},
timeout=30 # 增加超时时间
)
if response.status_code != 200:
error_msg = response.json().get('message', '未知错误')
raise ValueError(f"API调用失败:{error_msg}")
# 获取任务ID
task_id = response.json().get('output', {}).get('task_id')
if not task_id:
raise ValueError("未获取到任务ID")
print(f"✅ 任务已提交:{task_id}")
# 等待任务完成
max_retries = 120 # 增加到120次,
retry_interval = 10.0 # 保持10秒间隔
for i in range(max_retries):
print(f"⏳ 等待中... ({i+1}/{max_retries}, 已等待{(i+1) * retry_interval}秒)")
try:
status_response = requests.get(
f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30 # 增加超时时间到30秒
)
if status_response.status_code != 200:
error_msg = status_response.json().get('message', '未知错误')
raise ValueError(f"状态查询失败:{error_msg}")
result = status_response.json()
status = result.get('output', {}).get('task_status')
# 根据官方文档处理任务状态
if status == "SUCCEEDED":
video_url = result.get('output', {}).get('video_url')
if not video_url:
raise ValueError("未找到视频URL")
# 构建历史记录信息
history_info = f"""任务ID: {task_id}
模型: {model}
时长: {duration}秒
提示词: {prompt if prompt else '无'}
提示词扩展: {'开启' if prompt_extend == 'True' else '关闭'}
种子: {seed if seed != -1 else '随机'}
视频URL: {video_url}
生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}
总耗时: {(i+1) * retry_interval}秒"""
print(f"""
✅ 视频生成成功:
- 视频URL:{video_url}
- 任务ID:{task_id}
- 总耗时:{(i+1) * retry_interval}秒
""")
return (video_url, history_info)
elif status == "FAILED":
error = result.get('output', {}).get('message', '未知错误')
error_code = result.get('code', 'UNKNOWN')
error_details = {
'RequestError': '请求参数错误',
'InvalidApiKey': 'API密钥无效',
'QuotaExceeded': '配额超限',
'InternalError': '服务内部错误',
'TaskNotExist': '任务不存在'
}
error_msg = error_details.get(error_code, error)
raise ValueError(f"任务失败 [{error_code}]:{error_msg}")
elif status == "PENDING":
print(f"⏳ 任务排队中... ({i+1}/{max_retries})")
elif status == "RUNNING":
progress = result.get('output', {}).get('progress', 0)
print(f"🔄 任务执行中... 进度:{progress}%")
else:
print(f"⚠️ 未知状态:{status}")
print(f"📝 完整响应:{result}")
except requests.exceptions.RequestException as e:
print(f"⚠️ 网络错误 ({i+1}/{max_retries}): {str(e)}")
if i == max_retries - 1:
raise ValueError(f"网络连接失败: {str(e)}")
time.sleep(min(2 ** i, 30)) # 指数退避重试
continue
time.sleep(retry_interval)
raise ValueError(f"超过最大等待时间({max_retries * retry_interval}秒)")
except Exception as e:
error_msg = str(e)
print(f"❌ 视频生成失败:{error_msg}")
return (error_msg, "") # 失败时返回错误信息和空历史记录