Files
siraxe-ComfyUI-WanVideoWrap…/api.py
T
2026-03-14 22:59:56 +00:00

535 lines
21 KiB
Python

import json
import base64
from aiohttp import web
from server import PromptServer
import os
from PIL import Image
from io import BytesIO
import folder_paths
import subprocess
import sys
import cv2
import numpy as np
import os
import folder_paths
from datetime import datetime
# Import SAM2 masker for point-click masking
from .nodes.sam2_masker import sam2_masker
from .nodes.prepare_refs import convert_mask_to_contour
def get_bg_folder_path():
"""Get the path to the bg folder where A.jpg and bg_image.png are stored"""
# Look for the bg folder relative to this file
import pathlib
current_dir = pathlib.Path(__file__).parent
bg_path = current_dir / "web" / "power_spline_editor" / "bg"
# Create the directory if it doesn't exist
bg_path.mkdir(parents=True, exist_ok=True)
return str(bg_path)
def get_ref_folder_path():
"""Get the path to the ref folder where reference images are stored"""
import pathlib
current_dir = pathlib.Path(__file__).parent
ref_path = current_dir / "web" / "power_spline_editor" / "ref"
# Create the directory if it doesn't exist
ref_path.mkdir(parents=True, exist_ok=True)
return str(ref_path)
@PromptServer.instance.routes.post("/wanvideowrapper_qq/save_ref_image")
async def save_ref_image(request):
"""API endpoint to save ref_image to the bg folder"""
try:
post = await request.post()
image_data = post.get("image")
image_name = post.get("name", "bg_image.png")
if not image_data:
return web.json_response({"error": "No image data provided"}, status=400)
# Derive filename safely
safe_name = os.path.basename(image_name) or "bg_image.png"
if "." not in safe_name:
safe_name += ".png"
# Decode base64 image data
if image_data.startswith('data:image'):
# Remove data URL prefix (e.g., "data:image/png;base64,")
header, encoded = image_data.split(',', 1)
image_format = header.split('/')[1].split(';')[0] # Extract format like 'png' or 'jpeg'
else:
encoded = image_data
image_format = safe_name.split(".")[-1]
# Decode the base64 string
image_bytes = base64.b64decode(encoded)
# Verify it's a valid image
try:
img = Image.open(BytesIO(image_bytes))
img.verify() # Verify it's a valid image
img = Image.open(BytesIO(image_bytes)) # Reopen after verify
except Exception as e:
return web.json_response({"error": f"Invalid image data: {str(e)}"}, status=400)
# Get the bg folder path
bg_folder = get_bg_folder_path()
ref_image_path = os.path.join(bg_folder, safe_name)
# Decide format (prefer PNG to preserve alpha)
fmt = (image_format or "png").upper()
save_kwargs = {}
if fmt in ("JPG", "JPEG"):
img = img.convert('RGB')
fmt = "JPEG"
save_kwargs["quality"] = 95
else:
# Preserve alpha if present
if img.mode not in ("RGBA", "LA"):
img = img.convert("RGBA")
fmt = "PNG"
img.save(ref_image_path, format=fmt, **save_kwargs)
return web.json_response({"success": True, "path": ref_image_path, "message": "Reference image saved successfully"})
except Exception as e:
print(f"Error saving ref image: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/wanvideowrapper_qq/save_prepare_refs_images")
async def save_prepare_refs_images(request):
"""API endpoint to save ALL images from PrepareRefs node to the REF folder"""
try:
data = await request.json()
bg_image_data = data.get("bg_image")
ref_images_data = data.get("ref_images", [])
print(f"[API save_prepare_refs_images] === Received request ===")
print(f"[API save_prepare_refs_images] bg_image present: {bg_image_data is not None}")
print(f"[API save_prepare_refs_images] ref_images count: {len(ref_images_data) if isinstance(ref_images_data, list) else 0}")
saved_paths = {
"bg_image_path": None,
"ref_images_paths": []
}
# Get ref folder (ALL images go to ref folder for PrepareRefs!)
ref_folder = get_ref_folder_path()
print(f"[API save_prepare_refs_images] Using ref folder: {ref_folder}")
# Save background image to REF folder (not bg folder!)
if bg_image_data:
try:
print(f"[API save_prepare_refs_images] Saving bg_image to REF folder...")
# Decode base64 image data
if bg_image_data.startswith('data:image'):
header, encoded = bg_image_data.split(',', 1)
else:
encoded = bg_image_data
image_bytes = base64.b64decode(encoded)
img = Image.open(BytesIO(image_bytes))
# Save to REF folder as bg_image.png
bg_image_path = os.path.join(ref_folder, "bg_image.png")
img.save(bg_image_path, format="PNG")
saved_paths["bg_image_path"] = "ref/bg_image.png"
print(f"[API save_prepare_refs_images] ✓ Saved bg_image to {bg_image_path}")
except Exception as e:
print(f"[API save_prepare_refs_images] ERROR saving bg_image: {e}")
import traceback
traceback.print_exc()
# Save ref images to ref folder
if ref_images_data and isinstance(ref_images_data, list):
for idx, ref_image_data in enumerate(ref_images_data):
try:
print(f"[API save_prepare_refs_images] Saving ref_image {idx} to REF folder...")
# Decode base64 image data
if ref_image_data.startswith('data:image'):
header, encoded = ref_image_data.split(',', 1)
else:
encoded = ref_image_data
image_bytes = base64.b64decode(encoded)
img = Image.open(BytesIO(image_bytes))
# Save to ref folder
ref_image_path = os.path.join(ref_folder, f"ref_image_{idx}.png")
img.save(ref_image_path, format="PNG")
saved_paths["ref_images_paths"].append(f"ref/ref_image_{idx}.png")
print(f"[API save_prepare_refs_images] ✓ Saved ref_image_{idx} to {ref_image_path}")
except Exception as e:
print(f"[API save_prepare_refs_images] ERROR saving ref_image {idx}: {e}")
import traceback
traceback.print_exc()
print(f"[API save_prepare_refs_images] === COMPLETE ===")
print(f"[API save_prepare_refs_images] Saved bg_image: {saved_paths['bg_image_path']}")
print(f"[API save_prepare_refs_images] Saved {len(saved_paths['ref_images_paths'])} ref images")
return web.json_response({"success": True, "paths": saved_paths})
except Exception as e:
print(f"[API save_prepare_refs_images] ERROR: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
def get_lora_full_path(lora_name):
"""Get the full path to a LoRA file"""
try:
# Get the LoRA folder paths from ComfyUI
lora_folders = folder_paths.get_folder_paths("loras")
# Search for the LoRA file in all LoRA folders
for folder in lora_folders:
lora_path = os.path.join(folder, lora_name)
if os.path.exists(lora_path):
return os.path.abspath(lora_path)
# Also check for common extensions if not provided
if not lora_name.lower().endswith(('.safetensors', '.ckpt', '.pt')):
for ext in ['.safetensors', '.ckpt', '.pt']:
lora_path_with_ext = os.path.join(folder, lora_name + ext)
if os.path.exists(lora_path_with_ext):
return os.path.abspath(lora_path_with_ext)
return None
except Exception as e:
print(f"Error finding LoRA path: {e}")
return None
@PromptServer.instance.routes.post("/wanvideo_wrapper/open_explorer")
async def open_explorer(request):
"""API endpoint to open Windows Explorer at the LoRA file location"""
try:
if not sys.platform.startswith('win'):
return web.json_response({"error": "This feature is only available on Windows"}, status=400)
post = await request.post()
data = await request.json() if request.content_type == 'application/json' else {}
lora_name = data.get("lora_name") or post.get("lora_name")
if not lora_name:
return web.json_response({"error": "No LoRA name provided"}, status=400)
# Get the full path to the LoRA file
lora_path = get_lora_full_path(lora_name)
if not lora_path:
return web.json_response({"error": f"LoRA file not found: {lora_name}"}, status=404)
# Open Windows Explorer and select the file
try:
# Use start command to open Explorer with the file selected
subprocess.run(['explorer', '/select,', lora_path], check=True, shell=False)
return web.json_response({"success": True, "path": lora_path})
except subprocess.CalledProcessError as e:
# Fallback: open the folder if selecting the file fails
try:
folder_path = os.path.dirname(lora_path)
subprocess.run(['explorer', folder_path], check=True, shell=False)
return web.json_response({"success": True, "path": folder_path, "message": "Opened folder instead of selecting file"})
except subprocess.CalledProcessError as e2:
return web.json_response({"error": f"Failed to open Explorer: {str(e2)}"}, status=500)
except Exception as e:
print(f"Error opening Explorer: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/wanvideo_wrapper/get_lora_path")
async def get_lora_path(request):
"""API endpoint to get the full path of a LoRA file"""
try:
post = await request.post()
data = await request.json() if request.content_type == 'application/json' else {}
lora_name = data.get("lora_name") or post.get("lora_name")
if not lora_name:
return web.json_response({"error": "No LoRA name provided"}, status=400)
# Get the full path to the LoRA file
lora_path = get_lora_full_path(lora_name)
if not lora_path:
return web.json_response({"error": f"LoRA file not found: {lora_name}"}, status=404)
return web.json_response({"success": True, "path": lora_path})
except Exception as e:
print(f"Error getting LoRA path: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.post("/wanvideo_wrapper/rename_lora")
async def rename_lora(request):
"""API endpoint to rename a LoRA file and its associated files"""
try:
post = await request.post()
data = await request.json() if request.content_type == 'application/json' else {}
old_name = data.get("old_name") or post.get("old_name")
new_name = data.get("new_name") or post.get("new_name")
variant = data.get("variant") or post.get("variant", "high")
if not old_name:
return web.json_response({"error": "No old LoRA name provided"}, status=400)
if not new_name:
return web.json_response({"error": "No new LoRA name provided"}, status=400)
# Get the full path to the LoRA file
old_lora_path = get_lora_full_path(old_name)
if not old_lora_path:
return web.json_response({"error": f"LoRA file not found: {old_name}"}, status=404)
# Get the directory and file extension
lora_dir = os.path.dirname(old_lora_path)
old_base_name = os.path.splitext(os.path.basename(old_lora_path))[0]
lora_ext = os.path.splitext(old_lora_path)[1]
# Create the new file paths
new_lora_path = os.path.join(lora_dir, f"{new_name}{lora_ext}")
# Check if new file already exists
if os.path.exists(new_lora_path):
return web.json_response({"error": f"A file with the name '{new_name}{lora_ext}' already exists"}, status=409)
# Rename the main LoRA file
try:
os.rename(old_lora_path, new_lora_path)
print(f"Renamed LoRA: {old_lora_path} -> {new_lora_path}")
except Exception as e:
return web.json_response({"error": f"Failed to rename LoRA file: {str(e)}"}, status=500)
# Rename associated files (JSON and preview images)
renamed_files = [new_lora_path]
# Determine if this is a model or LoRA based on file path
is_model = any(old_lora_path.startswith(folder) for folder in folder_paths.get_folder_paths("checkpoints"))
if is_model:
# Handle model files - check checkpoints directory
model_folders = folder_paths.get_folder_paths("checkpoints")
for folder in model_folders:
# Look for _power_preview subfolder
preview_folder = os.path.join(folder, "_power_preview")
if os.path.exists(preview_folder):
renamed_files.extend(_rename_preview_files(old_base_name, new_name, preview_folder))
else:
# Handle LoRA files - check loras directory
lora_folders = folder_paths.get_folder_paths("loras")
for folder in lora_folders:
# Look for _power_preview subfolder
preview_folder = os.path.join(folder, "_power_preview")
if os.path.exists(preview_folder):
renamed_files.extend(_rename_preview_files(old_base_name, new_name, preview_folder))
return web.json_response({
"success": True,
"message": f"Successfully renamed LoRA and {len(renamed_files)-1} associated files",
"new_high_path": new_lora_path,
"new_low_path": new_lora_path, # For high variant, low path is the same
"renamed_files": renamed_files
})
except Exception as e:
print(f"Error renaming LoRA: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
def _rename_preview_files(old_base_name, new_name, preview_folder):
"""Helper function to rename preview files (JSON and images) in a preview folder."""
renamed_files = []
try:
# Rename JSON file
old_json_path = os.path.join(preview_folder, f"{old_base_name}.json")
new_json_path = os.path.join(preview_folder, f"{new_name}.json")
if os.path.exists(old_json_path):
try:
os.rename(old_json_path, new_json_path)
renamed_files.append(new_json_path)
print(f"Renamed JSON: {old_json_path} -> {new_json_path}")
except Exception as e:
print(f"Warning: Failed to rename JSON file: {e}")
# Rename preview images (with various suffixes)
for suffix in ["_01", "_02", "_03", "_thumb"]:
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
old_img_path = os.path.join(preview_folder, f"{old_base_name}{suffix}{ext}")
new_img_path = os.path.join(preview_folder, f"{new_name}{suffix}{ext}")
if os.path.exists(old_img_path):
try:
os.rename(old_img_path, new_img_path)
renamed_files.append(new_img_path)
print(f"Renamed image: {old_img_path} -> {new_img_path}")
except Exception as e:
print(f"Warning: Failed to rename image file: {e}")
# Also check for image without suffix (backward compatibility)
for ext in [".jpg", ".jpeg", ".png", ".webp"]:
old_img_path = os.path.join(preview_folder, f"{old_base_name}{ext}")
new_img_path = os.path.join(preview_folder, f"{new_name}{ext}")
if os.path.exists(old_img_path):
try:
os.rename(old_img_path, new_img_path)
renamed_files.append(new_img_path)
print(f"Renamed image: {old_img_path} -> {new_img_path}")
except Exception as e:
print(f"Warning: Failed to rename image file: {e}")
except Exception as e:
print(f"Error in _rename_preview_files: {e}")
return renamed_files
@PromptServer.instance.routes.post("/wanvideowrapper_qq/extract_first_frame")
async def extract_first_frame(request):
"""API endpoint to extract the first frame from a video file"""
try:
data = await request.json()
video_path = data.get("video_path")
if not video_path:
return web.json_response({"success": False, "error": "No video path provided"}, status=400)
print(f"[API extract_first_frame] Extracting first frame from: {video_path}")
# Try to find the video file
video_file_path = None
# Check if it's already an absolute path
if os.path.exists(video_path):
video_file_path = video_path
else:
# Search in input directories
input_folders = folder_paths.get_folder_paths("input")
for folder in input_folders:
potential_path = os.path.join(folder, video_path)
if os.path.exists(potential_path):
video_file_path = potential_path
break
if not video_file_path:
return web.json_response({"success": False, "error": f"Video file not found: {video_path}"}, status=404)
print(f"[API extract_first_frame] Found video at: {video_file_path}")
# Extract first frame using OpenCV
cap = cv2.VideoCapture(video_file_path)
if not cap.isOpened():
return web.json_response({"success": False, "error": "Failed to open video file"}, status=500)
# Read first frame
ret, frame = cap.read()
cap.release()
if not ret:
return web.json_response({"success": False, "error": "Failed to read first frame from video"}, status=500)
# Convert BGR to RGB
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Convert to PIL Image
img = Image.fromarray(frame_rgb)
# Convert to base64
buffered = BytesIO()
img.save(buffered, format="PNG")
img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
img_data_url = f"data:image/png;base64,{img_base64}"
print(f"[API extract_first_frame] Successfully extracted first frame")
return web.json_response({
"success": True,
"image": img_data_url
})
except Exception as e:
print(f"[API extract_first_frame] ERROR: {e}")
import traceback
traceback.print_exc()
return web.json_response({"success": False, "error": str(e)}, status=500)
@PromptServer.instance.routes.post("/wanvideowrapper_qq/sam2_predict")
async def sam2_predict(request):
"""
API endpoint for SAM2 point-click prediction.
Expects JSON with:
image: base64 encoded image (data URL format)
points: list of {'x': float, 'y': float, 'label': int} points
x, y are in pixel coordinates, label is 1 (foreground) or 0 (background)
Returns JSON with:
success: bool
path: list of {'x': float, 'y': float} normalized coordinates (0-1)
score: float (confidence score)
mask_shape: [height, width] of the predicted mask
"""
try:
data = await request.json()
# Validate required parameters
if not data.get("image"):
return web.json_response({"success": False, "error": "Missing image parameter"}, status=400)
if not data.get("points") or len(data["points"]) == 0:
return web.json_response({"success": False, "error": "Missing points parameter"}, status=400)
# Decode base64 image
image_data = data["image"]
if image_data.startswith('data:image'):
header, encoded = image_data.split(',', 1)
else:
encoded = image_data
image_bytes = base64.b64decode(encoded)
img = Image.open(BytesIO(image_bytes))
# Extract points
points = data["points"]
point_coords = [[p["x"], p["y"]] for p in points]
point_labels = [p.get("label", 1) for p in points] # Default to foreground
# Predict mask using SAM2
mask, score = sam2_masker.predict_from_points(img, point_coords, point_labels)
# Convert mask to contour path
path = convert_mask_to_contour(mask)
return web.json_response({
"success": True,
"path": path,
"score": score,
"mask_shape": mask.shape
})
except Exception as e:
import traceback
traceback.print_exc()
return web.json_response({"success": False, "error": str(e)}, status=500)