mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Enhance import functionality for recipes with image upload and URL support
- Added support for importing recipes via image upload or URL input in the ImportManager. - Implemented toggle functionality to switch between upload and URL modes, updating the UI accordingly. - Enhanced error handling for missing fields and invalid URLs during the import process. - Updated the RecipeRoutes to analyze images from both uploaded files and URLs, returning appropriate metadata. - Improved the import modal UI to accommodate new input methods and provide clearer user feedback.
This commit is contained in:
@@ -2,15 +2,17 @@ from .py.lora_manager import LoraManager
|
|||||||
from .py.nodes.lora_loader import LoraManagerLoader
|
from .py.nodes.lora_loader import LoraManagerLoader
|
||||||
from .py.nodes.trigger_word_toggle import TriggerWordToggle
|
from .py.nodes.trigger_word_toggle import TriggerWordToggle
|
||||||
from .py.nodes.lora_stacker import LoraStacker
|
from .py.nodes.lora_stacker import LoraStacker
|
||||||
|
from .py.nodes.save_image import SaveImage
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
LoraManagerLoader.NAME: LoraManagerLoader,
|
LoraManagerLoader.NAME: LoraManagerLoader,
|
||||||
TriggerWordToggle.NAME: TriggerWordToggle,
|
TriggerWordToggle.NAME: TriggerWordToggle,
|
||||||
LoraStacker.NAME: LoraStacker
|
LoraStacker.NAME: LoraStacker,
|
||||||
|
SaveImage.NAME: SaveImage
|
||||||
}
|
}
|
||||||
|
|
||||||
WEB_DIRECTORY = "./web/comfyui"
|
WEB_DIRECTORY = "./web/comfyui"
|
||||||
|
|
||||||
# Register routes on import
|
# Register routes on import
|
||||||
LoraManager.add_routes()
|
LoraManager.add_routes()
|
||||||
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
|
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
|
||||||
|
|||||||
41
py/nodes/save_image.py
Normal file
41
py/nodes/save_image.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import json
|
||||||
|
from server import PromptServer # type: ignore
|
||||||
|
|
||||||
|
class SaveImage:
|
||||||
|
NAME = "Save Image (LoraManager)"
|
||||||
|
CATEGORY = "Lora Manager/utils"
|
||||||
|
DESCRIPTION = "Experimental node to display image preview and print prompt and extra_pnginfo"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image": ("IMAGE",),
|
||||||
|
},
|
||||||
|
"hidden": {
|
||||||
|
"prompt": "PROMPT",
|
||||||
|
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
RETURN_NAMES = ("image",)
|
||||||
|
FUNCTION = "process_image"
|
||||||
|
|
||||||
|
def process_image(self, image, prompt=None, extra_pnginfo=None):
|
||||||
|
# Print the prompt information
|
||||||
|
print("SaveImage Node - Prompt:")
|
||||||
|
if prompt:
|
||||||
|
print(json.dumps(prompt, indent=2))
|
||||||
|
else:
|
||||||
|
print("No prompt information available")
|
||||||
|
|
||||||
|
# Print the extra_pnginfo
|
||||||
|
print("\nSaveImage Node - Extra PNG Info:")
|
||||||
|
if extra_pnginfo:
|
||||||
|
print(json.dumps(extra_pnginfo, indent=2))
|
||||||
|
else:
|
||||||
|
print("No extra PNG info available")
|
||||||
|
|
||||||
|
# Return the image unchanged
|
||||||
|
return (image,)
|
||||||
@@ -190,45 +190,90 @@ class RecipeRoutes:
|
|||||||
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
return datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
async def analyze_recipe_image(self, request: web.Request) -> web.Response:
|
async def analyze_recipe_image(self, request: web.Request) -> web.Response:
|
||||||
"""Analyze an uploaded image for recipe metadata"""
|
"""Analyze an uploaded image or URL for recipe metadata"""
|
||||||
temp_path = None
|
temp_path = None
|
||||||
try:
|
try:
|
||||||
reader = await request.multipart()
|
# Check if request contains multipart data (image) or JSON data (url)
|
||||||
field = await reader.next()
|
content_type = request.headers.get('Content-Type', '')
|
||||||
|
|
||||||
if field.name != 'image':
|
is_url_mode = False
|
||||||
return web.json_response({
|
|
||||||
"error": "No image field found",
|
|
||||||
"loras": []
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Create a temporary file to store the uploaded image
|
if 'multipart/form-data' in content_type:
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
|
# Handle image upload
|
||||||
while True:
|
reader = await request.multipart()
|
||||||
chunk = await field.read_chunk()
|
field = await reader.next()
|
||||||
if not chunk:
|
|
||||||
break
|
if field.name != 'image':
|
||||||
temp_file.write(chunk)
|
return web.json_response({
|
||||||
temp_path = temp_file.name
|
"error": "No image field found",
|
||||||
|
"loras": []
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Create a temporary file to store the uploaded image
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
|
||||||
|
while True:
|
||||||
|
chunk = await field.read_chunk()
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
temp_file.write(chunk)
|
||||||
|
temp_path = temp_file.name
|
||||||
|
|
||||||
|
elif 'application/json' in content_type:
|
||||||
|
# Handle URL input
|
||||||
|
data = await request.json()
|
||||||
|
url = data.get('url')
|
||||||
|
is_url_mode = True
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "No URL provided",
|
||||||
|
"loras": []
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# Download image from URL
|
||||||
|
from ..utils.utils import download_twitter_image
|
||||||
|
temp_path = download_twitter_image(url)
|
||||||
|
|
||||||
|
if not temp_path:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Failed to download image from URL",
|
||||||
|
"loras": []
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
# Extract metadata from the image using ExifUtils
|
# Extract metadata from the image using ExifUtils
|
||||||
user_comment = ExifUtils.extract_user_comment(temp_path)
|
user_comment = ExifUtils.extract_user_comment(temp_path)
|
||||||
|
|
||||||
# If no metadata found, return a more specific error
|
# If no metadata found, return a more specific error
|
||||||
if not user_comment:
|
if not user_comment:
|
||||||
return web.json_response({
|
result = {
|
||||||
"error": "No metadata found in this image",
|
"error": "No metadata found in this image",
|
||||||
"loras": [] # Return empty loras array to prevent client-side errors
|
"loras": [] # Return empty loras array to prevent client-side errors
|
||||||
}, status=200) # Return 200 instead of 400 to handle gracefully
|
}
|
||||||
|
|
||||||
|
# For URL mode, include the image data as base64
|
||||||
|
if is_url_mode and temp_path:
|
||||||
|
import base64
|
||||||
|
with open(temp_path, "rb") as image_file:
|
||||||
|
result["image_base64"] = base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
return web.json_response(result, status=200)
|
||||||
|
|
||||||
# Use the parser factory to get the appropriate parser
|
# Use the parser factory to get the appropriate parser
|
||||||
parser = RecipeParserFactory.create_parser(user_comment)
|
parser = RecipeParserFactory.create_parser(user_comment)
|
||||||
|
|
||||||
if parser is None:
|
if parser is None:
|
||||||
return web.json_response({
|
result = {
|
||||||
"error": "No parser found for this image",
|
"error": "No parser found for this image",
|
||||||
"loras": [] # Return empty loras array to prevent client-side errors
|
"loras": [] # Return empty loras array to prevent client-side errors
|
||||||
}, status=200) # Return 200 instead of 400 to handle gracefully
|
}
|
||||||
|
|
||||||
|
# For URL mode, include the image data as base64
|
||||||
|
if is_url_mode and temp_path:
|
||||||
|
import base64
|
||||||
|
with open(temp_path, "rb") as image_file:
|
||||||
|
result["image_base64"] = base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
|
|
||||||
|
return web.json_response(result, status=200)
|
||||||
|
|
||||||
# Parse the metadata
|
# Parse the metadata
|
||||||
result = await parser.parse_metadata(
|
result = await parser.parse_metadata(
|
||||||
@@ -237,6 +282,12 @@ class RecipeRoutes:
|
|||||||
civitai_client=self.civitai_client
|
civitai_client=self.civitai_client
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# For URL mode, include the image data as base64
|
||||||
|
if is_url_mode and temp_path:
|
||||||
|
import base64
|
||||||
|
with open(temp_path, "rb") as image_file:
|
||||||
|
result["image_base64"] = base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
|
|
||||||
# Check for errors
|
# Check for errors
|
||||||
if "error" in result and not result.get("loras"):
|
if "error" in result and not result.get("loras"):
|
||||||
return web.json_response(result, status=200)
|
return web.json_response(result, status=200)
|
||||||
@@ -265,6 +316,8 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
# Process form data
|
# Process form data
|
||||||
image = None
|
image = None
|
||||||
|
image_base64 = None
|
||||||
|
image_url = None
|
||||||
name = None
|
name = None
|
||||||
tags = []
|
tags = []
|
||||||
metadata = None
|
metadata = None
|
||||||
@@ -284,6 +337,14 @@ class RecipeRoutes:
|
|||||||
image_data += chunk
|
image_data += chunk
|
||||||
image = image_data
|
image = image_data
|
||||||
|
|
||||||
|
elif field.name == 'image_base64':
|
||||||
|
# Get base64 image data
|
||||||
|
image_base64 = await field.text()
|
||||||
|
|
||||||
|
elif field.name == 'image_url':
|
||||||
|
# Get image URL
|
||||||
|
image_url = await field.text()
|
||||||
|
|
||||||
elif field.name == 'name':
|
elif field.name == 'name':
|
||||||
name = await field.text()
|
name = await field.text()
|
||||||
|
|
||||||
@@ -301,8 +362,44 @@ class RecipeRoutes:
|
|||||||
except:
|
except:
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
||||||
if not image or not name or not metadata:
|
missing_fields = []
|
||||||
return web.json_response({"error": "Missing required fields"}, status=400)
|
if not name:
|
||||||
|
missing_fields.append("name")
|
||||||
|
if not metadata:
|
||||||
|
missing_fields.append("metadata")
|
||||||
|
if missing_fields:
|
||||||
|
return web.json_response({"error": f"Missing required fields: {', '.join(missing_fields)}"}, status=400)
|
||||||
|
|
||||||
|
# Handle different image sources
|
||||||
|
if not image:
|
||||||
|
if image_base64:
|
||||||
|
# Convert base64 to binary
|
||||||
|
import base64
|
||||||
|
try:
|
||||||
|
# Remove potential data URL prefix
|
||||||
|
if ',' in image_base64:
|
||||||
|
image_base64 = image_base64.split(',', 1)[1]
|
||||||
|
image = base64.b64decode(image_base64)
|
||||||
|
except Exception as e:
|
||||||
|
return web.json_response({"error": f"Invalid base64 image data: {str(e)}"}, status=400)
|
||||||
|
elif image_url:
|
||||||
|
# Download image from URL
|
||||||
|
from ..utils.utils import download_twitter_image
|
||||||
|
temp_path = download_twitter_image(image_url)
|
||||||
|
if not temp_path:
|
||||||
|
return web.json_response({"error": "Failed to download image from URL"}, status=400)
|
||||||
|
|
||||||
|
# Read the downloaded image
|
||||||
|
with open(temp_path, 'rb') as f:
|
||||||
|
image = f.read()
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
try:
|
||||||
|
os.unlink(temp_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return web.json_response({"error": "No image data provided"}, status=400)
|
||||||
|
|
||||||
# Create recipes directory if it doesn't exist
|
# Create recipes directory if it doesn't exist
|
||||||
recipes_dir = self.recipe_scanner.recipes_dir
|
recipes_dir = self.recipe_scanner.recipes_dir
|
||||||
@@ -625,4 +722,4 @@ class RecipeRoutes:
|
|||||||
# Remove from dictionary
|
# Remove from dictionary
|
||||||
del self._shared_recipes[rid]
|
del self._shared_recipes[rid]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error cleaning up shared recipe {rid}: {e}")
|
logger.error(f"Error cleaning up shared recipe {rid}: {e}")
|
||||||
|
|||||||
41
py/utils/utils.py
Normal file
41
py/utils/utils.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
def download_twitter_image(url):
|
||||||
|
"""Download image from a URL containing twitter:image meta tag
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL to download image from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Path to downloaded temporary image file
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Download page content
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Parse HTML
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Find twitter:image meta tag
|
||||||
|
meta_tag = soup.find('meta', attrs={'property': 'twitter:image'})
|
||||||
|
if not meta_tag:
|
||||||
|
return None
|
||||||
|
|
||||||
|
image_url = meta_tag['content']
|
||||||
|
|
||||||
|
# Download image
|
||||||
|
image_response = requests.get(image_url)
|
||||||
|
image_response.raise_for_status()
|
||||||
|
|
||||||
|
# Save to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
|
||||||
|
temp_file.write(image_response.content)
|
||||||
|
return temp_file.name
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading twitter image: {e}")
|
||||||
|
return None
|
||||||
@@ -4,6 +4,47 @@
|
|||||||
transition: none !important; /* Disable any transitions that might affect display */
|
transition: none !important; /* Disable any transitions that might affect display */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Import Mode Toggle */
|
||||||
|
.import-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:first-child {
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn.active {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover:not(.active) {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-section {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
/* File Input Styles */
|
/* File Input Styles */
|
||||||
.file-input-wrapper {
|
.file-input-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -364,6 +405,14 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group button {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: var(--lora-error);
|
color: var(--lora-error);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export class ImportManager {
|
|||||||
|
|
||||||
// 添加对注入样式的引用
|
// 添加对注入样式的引用
|
||||||
this.injectedStyles = null;
|
this.injectedStyles = null;
|
||||||
|
|
||||||
|
// Add import mode tracking
|
||||||
|
this.importMode = 'upload'; // Default mode: 'upload' or 'url'
|
||||||
}
|
}
|
||||||
|
|
||||||
showImportModal() {
|
showImportModal() {
|
||||||
@@ -80,16 +83,21 @@ export class ImportManager {
|
|||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset error message
|
// Reset URL input
|
||||||
const errorElement = document.getElementById('uploadError');
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
if (errorElement) {
|
if (urlInput) {
|
||||||
errorElement.textContent = '';
|
urlInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset preview
|
// Reset error messages
|
||||||
const previewElement = document.getElementById('imagePreview');
|
const uploadError = document.getElementById('uploadError');
|
||||||
if (previewElement) {
|
if (uploadError) {
|
||||||
previewElement.innerHTML = '<div class="placeholder">Image preview will appear here</div>';
|
uploadError.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlError = document.getElementById('urlError');
|
||||||
|
if (urlError) {
|
||||||
|
urlError.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset recipe name input
|
// Reset recipe name input
|
||||||
@@ -111,6 +119,10 @@ export class ImportManager {
|
|||||||
this.recipeTags = [];
|
this.recipeTags = [];
|
||||||
this.missingLoras = [];
|
this.missingLoras = [];
|
||||||
|
|
||||||
|
// Reset import mode to upload
|
||||||
|
this.importMode = 'upload';
|
||||||
|
this.toggleImportMode('upload');
|
||||||
|
|
||||||
// Clear selected folder and remove selection from UI
|
// Clear selected folder and remove selection from UI
|
||||||
this.selectedFolder = '';
|
this.selectedFolder = '';
|
||||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||||
@@ -132,6 +144,45 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleImportMode(mode) {
|
||||||
|
this.importMode = mode;
|
||||||
|
|
||||||
|
// Update toggle buttons
|
||||||
|
const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]');
|
||||||
|
const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]');
|
||||||
|
|
||||||
|
if (uploadBtn && urlBtn) {
|
||||||
|
if (mode === 'upload') {
|
||||||
|
uploadBtn.classList.add('active');
|
||||||
|
urlBtn.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
uploadBtn.classList.remove('active');
|
||||||
|
urlBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide appropriate sections
|
||||||
|
const uploadSection = document.getElementById('uploadSection');
|
||||||
|
const urlSection = document.getElementById('urlSection');
|
||||||
|
|
||||||
|
if (uploadSection && urlSection) {
|
||||||
|
if (mode === 'upload') {
|
||||||
|
uploadSection.style.display = 'block';
|
||||||
|
urlSection.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
uploadSection.style.display = 'none';
|
||||||
|
urlSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear error messages
|
||||||
|
const uploadError = document.getElementById('uploadError');
|
||||||
|
const urlError = document.getElementById('urlError');
|
||||||
|
|
||||||
|
if (uploadError) uploadError.textContent = '';
|
||||||
|
if (urlError) urlError.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
handleImageUpload(event) {
|
handleImageUpload(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
const errorElement = document.getElementById('uploadError');
|
const errorElement = document.getElementById('uploadError');
|
||||||
@@ -154,6 +205,85 @@ export class ImportManager {
|
|||||||
this.uploadAndAnalyzeImage();
|
this.uploadAndAnalyzeImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleUrlInput() {
|
||||||
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
|
const errorElement = document.getElementById('urlError');
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
|
||||||
|
// Validate URL
|
||||||
|
if (!url) {
|
||||||
|
errorElement.textContent = 'Please enter a URL';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
errorElement.textContent = 'Please enter a valid URL';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset error
|
||||||
|
errorElement.textContent = '';
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
this.loadingManager.showSimpleLoading('Fetching image from URL...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call API to analyze the URL
|
||||||
|
await this.analyzeImageFromUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
errorElement.textContent = error.message || 'Failed to fetch image from URL';
|
||||||
|
} finally {
|
||||||
|
this.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async analyzeImageFromUrl(url) {
|
||||||
|
try {
|
||||||
|
// Call the API with URL data
|
||||||
|
const response = await fetch('/api/recipes/analyze-image', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ url: url })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to analyze image from URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recipe data from response
|
||||||
|
this.recipeData = await response.json();
|
||||||
|
|
||||||
|
// Check if we have an error message
|
||||||
|
if (this.recipeData.error) {
|
||||||
|
throw new Error(this.recipeData.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have valid recipe data
|
||||||
|
if (!this.recipeData || !this.recipeData.loras || this.recipeData.loras.length === 0) {
|
||||||
|
throw new Error('No LoRA information found in this image');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store generation parameters if available
|
||||||
|
if (this.recipeData.gen_params) {
|
||||||
|
console.log('Generation parameters found:', this.recipeData.gen_params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find missing LoRAs
|
||||||
|
this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally);
|
||||||
|
|
||||||
|
// Proceed to recipe details step
|
||||||
|
this.showRecipeDetailsStep();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error analyzing URL:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async uploadAndAnalyzeImage() {
|
async uploadAndAnalyzeImage() {
|
||||||
if (!this.recipeImage) {
|
if (!this.recipeImage) {
|
||||||
showToast('Please select an image first', 'error');
|
showToast('Please select an image first', 'error');
|
||||||
@@ -172,7 +302,7 @@ export class ImportManager {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get recipe data from response
|
// Get recipe data from response
|
||||||
this.recipeData = await response.json();
|
this.recipeData = await response.json();
|
||||||
|
|
||||||
@@ -256,12 +386,24 @@ export class ImportManager {
|
|||||||
|
|
||||||
// Display the uploaded image in the preview
|
// Display the uploaded image in the preview
|
||||||
const imagePreview = document.getElementById('recipeImagePreview');
|
const imagePreview = document.getElementById('recipeImagePreview');
|
||||||
if (imagePreview && this.recipeImage) {
|
if (imagePreview) {
|
||||||
const reader = new FileReader();
|
if (this.recipeImage) {
|
||||||
reader.onload = (e) => {
|
// For file upload mode
|
||||||
imagePreview.innerHTML = `<img src="${e.target.result}" alt="Recipe preview">`;
|
const reader = new FileReader();
|
||||||
};
|
reader.onload = (e) => {
|
||||||
reader.readAsDataURL(this.recipeImage);
|
imagePreview.innerHTML = `<img src="${e.target.result}" alt="Recipe preview">`;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(this.recipeImage);
|
||||||
|
} else if (this.recipeData && this.recipeData.image_base64) {
|
||||||
|
// For URL mode - use the base64 image data returned from the backend
|
||||||
|
imagePreview.innerHTML = `<img src="data:image/jpeg;base64,${this.recipeData.image_base64}" alt="Recipe preview">`;
|
||||||
|
} else if (this.importMode === 'url') {
|
||||||
|
// Fallback for URL mode if no base64 data
|
||||||
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
|
if (urlInput && urlInput.value) {
|
||||||
|
imagePreview.innerHTML = `<img src="${urlInput.value}" alt="Recipe preview" crossorigin="anonymous">`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update LoRA count information
|
// Update LoRA count information
|
||||||
@@ -577,10 +719,21 @@ export class ImportManager {
|
|||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset URL input
|
||||||
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
|
if (urlInput) {
|
||||||
|
urlInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Clear any previous error messages
|
// Clear any previous error messages
|
||||||
const errorElement = document.getElementById('uploadError');
|
const uploadError = document.getElementById('uploadError');
|
||||||
if (errorElement) {
|
if (uploadError) {
|
||||||
errorElement.textContent = '';
|
uploadError.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlError = document.getElementById('urlError');
|
||||||
|
if (urlError) {
|
||||||
|
urlError.textContent = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,7 +753,26 @@ export class ImportManager {
|
|||||||
|
|
||||||
// Create form data for save request
|
// Create form data for save request
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', this.recipeImage);
|
|
||||||
|
// Handle image data - either from file upload or from URL mode
|
||||||
|
if (this.recipeImage) {
|
||||||
|
// File upload mode
|
||||||
|
formData.append('image', this.recipeImage);
|
||||||
|
} else if (this.recipeData && this.recipeData.image_base64) {
|
||||||
|
// URL mode with base64 data
|
||||||
|
formData.append('image_base64', this.recipeData.image_base64);
|
||||||
|
} else if (this.importMode === 'url') {
|
||||||
|
// Fallback for URL mode - tell backend to fetch the image again
|
||||||
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
|
if (urlInput && urlInput.value) {
|
||||||
|
formData.append('image_url', urlInput.value);
|
||||||
|
} else {
|
||||||
|
throw new Error('No image data available');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No image data available');
|
||||||
|
}
|
||||||
|
|
||||||
formData.append('name', this.recipeName);
|
formData.append('name', this.recipeName);
|
||||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||||
|
|
||||||
|
|||||||
@@ -3,19 +3,45 @@
|
|||||||
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
||||||
<h2>Import Recipe</h2>
|
<h2>Import Recipe</h2>
|
||||||
|
|
||||||
<!-- Step 1: Upload Image -->
|
<!-- Step 1: Upload Image or Input URL -->
|
||||||
<div class="import-step" id="uploadStep">
|
<div class="import-step" id="uploadStep">
|
||||||
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
<div class="import-mode-toggle">
|
||||||
|
<button class="toggle-btn active" data-mode="upload" onclick="importManager.toggleImportMode('upload')">
|
||||||
|
<i class="fas fa-upload"></i> Upload Image
|
||||||
|
</button>
|
||||||
|
<button class="toggle-btn" data-mode="url" onclick="importManager.toggleImportMode('url')">
|
||||||
|
<i class="fas fa-link"></i> Input URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<!-- Upload Image Section -->
|
||||||
<label for="recipeImageUpload">Select Image:</label>
|
<div class="import-section" id="uploadSection">
|
||||||
<div class="file-input-wrapper">
|
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
||||||
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
<div class="input-group">
|
||||||
<div class="file-input-button">
|
<label for="recipeImageUpload">Select Image:</label>
|
||||||
<i class="fas fa-upload"></i> Select Image
|
<div class="file-input-wrapper">
|
||||||
|
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
||||||
|
<div class="file-input-button">
|
||||||
|
<i class="fas fa-upload"></i> Select Image
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="error-message" id="uploadError"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input URL Section -->
|
||||||
|
<div class="import-section" id="urlSection" style="display: none;">
|
||||||
|
<p>Input a Civitai image URL to import as a recipe.</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="imageUrlInput">Image URL:</label>
|
||||||
|
<input type="text" id="imageUrlInput" placeholder="https://civitai.com/images/...">
|
||||||
|
<div class="error-message" id="urlError"></div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<button class="primary-btn" onclick="importManager.handleUrlInput()">
|
||||||
|
<i class="fas fa-download"></i> Fetch Image
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="error-message" id="uploadError"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -113,4 +139,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user