mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Add support for remote video analysis and preview for recipe imports. see #420
This commit is contained in:
@@ -437,6 +437,7 @@ class RecipeManagementHandler:
|
|||||||
name=payload["name"],
|
name=payload["name"],
|
||||||
tags=payload["tags"],
|
tags=payload["tags"],
|
||||||
metadata=payload["metadata"],
|
metadata=payload["metadata"],
|
||||||
|
extension=payload.get("extension"),
|
||||||
)
|
)
|
||||||
return web.json_response(result.payload, status=result.status)
|
return web.json_response(result.payload, status=result.status)
|
||||||
except RecipeValidationError as exc:
|
except RecipeValidationError as exc:
|
||||||
@@ -625,6 +626,7 @@ class RecipeManagementHandler:
|
|||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
tags: list[str] = []
|
tags: list[str] = []
|
||||||
metadata: Optional[Dict[str, Any]] = None
|
metadata: Optional[Dict[str, Any]] = None
|
||||||
|
extension: Optional[str] = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
field = await reader.next()
|
field = await reader.next()
|
||||||
@@ -655,6 +657,8 @@ class RecipeManagementHandler:
|
|||||||
metadata = json.loads(metadata_text)
|
metadata = json.loads(metadata_text)
|
||||||
except Exception:
|
except Exception:
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
elif field.name == "extension":
|
||||||
|
extension = await field.text()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"image_bytes": image_bytes,
|
"image_bytes": image_bytes,
|
||||||
@@ -662,6 +666,7 @@ class RecipeManagementHandler:
|
|||||||
"name": name,
|
"name": name,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
|
"extension": extension,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _parse_tags(self, tag_text: Optional[str]) -> list[str]:
|
def _parse_tags(self, tag_text: Optional[str]) -> list[str]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import numpy as np
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from ...utils.utils import calculate_recipe_fingerprint
|
from ...utils.utils import calculate_recipe_fingerprint
|
||||||
|
from ...utils.civitai_utils import rewrite_preview_url
|
||||||
from .errors import (
|
from .errors import (
|
||||||
RecipeDownloadError,
|
RecipeDownloadError,
|
||||||
RecipeNotFoundError,
|
RecipeNotFoundError,
|
||||||
@@ -94,18 +95,39 @@ class RecipeAnalysisService:
|
|||||||
if civitai_client is None:
|
if civitai_client is None:
|
||||||
raise RecipeServiceError("Civitai client unavailable")
|
raise RecipeServiceError("Civitai client unavailable")
|
||||||
|
|
||||||
temp_path = self._create_temp_path()
|
temp_path = None
|
||||||
metadata: Optional[dict[str, Any]] = None
|
metadata: Optional[dict[str, Any]] = None
|
||||||
|
is_video = False
|
||||||
|
extension = ".jpg" # Default
|
||||||
|
|
||||||
try:
|
try:
|
||||||
civitai_match = re.match(r"https://civitai\.com/images/(\d+)", url)
|
civitai_match = re.match(r"https://civitai\.com/images/(\d+)", url)
|
||||||
if civitai_match:
|
if civitai_match:
|
||||||
image_info = await civitai_client.get_image_info(civitai_match.group(1))
|
image_info = await civitai_client.get_image_info(civitai_match.group(1))
|
||||||
if not image_info:
|
if not image_info:
|
||||||
raise RecipeDownloadError("Failed to fetch image information from Civitai")
|
raise RecipeDownloadError("Failed to fetch image information from Civitai")
|
||||||
|
|
||||||
image_url = image_info.get("url")
|
image_url = image_info.get("url")
|
||||||
if not image_url:
|
if not image_url:
|
||||||
raise RecipeDownloadError("No image URL found in Civitai response")
|
raise RecipeDownloadError("No image URL found in Civitai response")
|
||||||
|
|
||||||
|
is_video = image_info.get("type") == "video"
|
||||||
|
|
||||||
|
# Use optimized preview URLs if possible
|
||||||
|
rewritten_url, _ = rewrite_preview_url(image_url, media_type=image_info.get("type"))
|
||||||
|
if rewritten_url:
|
||||||
|
image_url = rewritten_url
|
||||||
|
|
||||||
|
if is_video:
|
||||||
|
# Extract extension from URL
|
||||||
|
url_path = image_url.split('?')[0].split('#')[0]
|
||||||
|
extension = os.path.splitext(url_path)[1].lower() or ".mp4"
|
||||||
|
else:
|
||||||
|
extension = ".jpg"
|
||||||
|
|
||||||
|
temp_path = self._create_temp_path(suffix=extension)
|
||||||
await self._download_image(image_url, temp_path)
|
await self._download_image(image_url, temp_path)
|
||||||
|
|
||||||
metadata = image_info.get("meta") if "meta" in image_info else None
|
metadata = image_info.get("meta") if "meta" in image_info else None
|
||||||
if (
|
if (
|
||||||
isinstance(metadata, dict)
|
isinstance(metadata, dict)
|
||||||
@@ -114,22 +136,31 @@ class RecipeAnalysisService:
|
|||||||
):
|
):
|
||||||
metadata = metadata["meta"]
|
metadata = metadata["meta"]
|
||||||
else:
|
else:
|
||||||
|
# Basic extension detection for non-Civitai URLs
|
||||||
|
url_path = url.split('?')[0].split('#')[0]
|
||||||
|
extension = os.path.splitext(url_path)[1].lower()
|
||||||
|
if extension in [".mp4", ".webm"]:
|
||||||
|
is_video = True
|
||||||
|
else:
|
||||||
|
extension = ".jpg"
|
||||||
|
|
||||||
|
temp_path = self._create_temp_path(suffix=extension)
|
||||||
await self._download_image(url, temp_path)
|
await self._download_image(url, temp_path)
|
||||||
|
|
||||||
if metadata is None:
|
if metadata is None and not is_video:
|
||||||
metadata = self._exif_utils.extract_image_metadata(temp_path)
|
metadata = self._exif_utils.extract_image_metadata(temp_path)
|
||||||
|
|
||||||
if not metadata:
|
|
||||||
return self._metadata_not_found_response(temp_path)
|
|
||||||
|
|
||||||
return await self._parse_metadata(
|
return await self._parse_metadata(
|
||||||
metadata,
|
metadata or {},
|
||||||
recipe_scanner=recipe_scanner,
|
recipe_scanner=recipe_scanner,
|
||||||
image_path=temp_path,
|
image_path=temp_path,
|
||||||
include_image_base64=True,
|
include_image_base64=True,
|
||||||
|
is_video=is_video,
|
||||||
|
extension=extension,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self._safe_cleanup(temp_path)
|
if temp_path:
|
||||||
|
self._safe_cleanup(temp_path)
|
||||||
|
|
||||||
async def analyze_local_image(
|
async def analyze_local_image(
|
||||||
self,
|
self,
|
||||||
@@ -198,12 +229,16 @@ class RecipeAnalysisService:
|
|||||||
recipe_scanner,
|
recipe_scanner,
|
||||||
image_path: Optional[str],
|
image_path: Optional[str],
|
||||||
include_image_base64: bool,
|
include_image_base64: bool,
|
||||||
|
is_video: bool = False,
|
||||||
|
extension: str = ".jpg",
|
||||||
) -> AnalysisResult:
|
) -> AnalysisResult:
|
||||||
parser = self._recipe_parser_factory.create_parser(metadata)
|
parser = self._recipe_parser_factory.create_parser(metadata)
|
||||||
if parser is None:
|
if parser is None:
|
||||||
payload = {"error": "No parser found for this image", "loras": []}
|
payload = {"error": "No parser found for this image", "loras": []}
|
||||||
if include_image_base64 and image_path:
|
if include_image_base64 and image_path:
|
||||||
payload["image_base64"] = self._encode_file(image_path)
|
payload["image_base64"] = self._encode_file(image_path)
|
||||||
|
payload["is_video"] = is_video
|
||||||
|
payload["extension"] = extension
|
||||||
return AnalysisResult(payload)
|
return AnalysisResult(payload)
|
||||||
|
|
||||||
result = await parser.parse_metadata(metadata, recipe_scanner=recipe_scanner)
|
result = await parser.parse_metadata(metadata, recipe_scanner=recipe_scanner)
|
||||||
@@ -211,6 +246,9 @@ class RecipeAnalysisService:
|
|||||||
if include_image_base64 and image_path:
|
if include_image_base64 and image_path:
|
||||||
result["image_base64"] = self._encode_file(image_path)
|
result["image_base64"] = self._encode_file(image_path)
|
||||||
|
|
||||||
|
result["is_video"] = is_video
|
||||||
|
result["extension"] = extension
|
||||||
|
|
||||||
if "error" in result and not result.get("loras"):
|
if "error" in result and not result.get("loras"):
|
||||||
return AnalysisResult(result)
|
return AnalysisResult(result)
|
||||||
|
|
||||||
@@ -241,8 +279,8 @@ class RecipeAnalysisService:
|
|||||||
temp_file.write(data)
|
temp_file.write(data)
|
||||||
return temp_file.name
|
return temp_file.name
|
||||||
|
|
||||||
def _create_temp_path(self) -> str:
|
def _create_temp_path(self, suffix: str = ".jpg") -> str:
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_file:
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
|
||||||
return temp_file.name
|
return temp_file.name
|
||||||
|
|
||||||
def _safe_cleanup(self, path: Optional[str]) -> None:
|
def _safe_cleanup(self, path: Optional[str]) -> None:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* Import Modal Styles */
|
/* Import Modal Styles */
|
||||||
.import-step {
|
.import-step {
|
||||||
margin: var(--space-2) 0;
|
margin: var(--space-2) 0;
|
||||||
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 */
|
||||||
@@ -107,7 +108,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-image img {
|
.recipe-image img,
|
||||||
|
.recipe-preview-video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
@@ -379,7 +381,7 @@
|
|||||||
.recipe-details-layout {
|
.recipe-details-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipe-image-container {
|
.recipe-image-container {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
@@ -512,14 +514,17 @@
|
|||||||
|
|
||||||
/* Prevent layout shift with scrollbar */
|
/* Prevent layout shift with scrollbar */
|
||||||
.modal-content {
|
.modal-content {
|
||||||
overflow-y: scroll; /* Always show scrollbar */
|
overflow-y: scroll;
|
||||||
scrollbar-gutter: stable; /* Reserve space for scrollbar */
|
/* Always show scrollbar */
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
/* Reserve space for scrollbar */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For browsers that don't support scrollbar-gutter */
|
/* For browsers that don't support scrollbar-gutter */
|
||||||
@supports not (scrollbar-gutter: stable) {
|
@supports not (scrollbar-gutter: stable) {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
padding-right: calc(var(--space-2) + var(--scrollbar-width)); /* Add extra padding for scrollbar */
|
padding-right: calc(var(--space-2) + var(--scrollbar-width));
|
||||||
|
/* Add extra padding for scrollbar */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +591,8 @@
|
|||||||
|
|
||||||
/* Remove the old warning-message styles that were causing layout issues */
|
/* Remove the old warning-message styles that were causing layout issues */
|
||||||
.warning-message {
|
.warning-message {
|
||||||
display: none; /* Hide the old style */
|
display: none;
|
||||||
|
/* Hide the old style */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update deleted badge to be more prominent */
|
/* Update deleted badge to be more prominent */
|
||||||
@@ -613,7 +619,8 @@
|
|||||||
color: var(--lora-error);
|
color: var(--lora-error);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
min-height: 20px; /* Ensure there's always space for the error message */
|
min-height: 20px;
|
||||||
|
/* Ensure there's always space for the error message */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,8 +669,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(-10px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicate-warning {
|
.duplicate-warning {
|
||||||
@@ -779,6 +793,7 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,9 +808,9 @@
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicate-recipe-date,
|
.duplicate-recipe-date,
|
||||||
.duplicate-recipe-lora-count {
|
.duplicate-recipe-lora-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
@@ -12,21 +12,21 @@ export class DownloadManager {
|
|||||||
async saveRecipe() {
|
async saveRecipe() {
|
||||||
// Check if we're in download-only mode (for existing recipe)
|
// Check if we're in download-only mode (for existing recipe)
|
||||||
const isDownloadOnly = !!this.importManager.recipeId;
|
const isDownloadOnly = !!this.importManager.recipeId;
|
||||||
|
|
||||||
if (!isDownloadOnly && !this.importManager.recipeName) {
|
if (!isDownloadOnly && !this.importManager.recipeName) {
|
||||||
showToast('toast.recipes.enterRecipeName', {}, 'error');
|
showToast('toast.recipes.enterRecipeName', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show progress indicator
|
// Show progress indicator
|
||||||
this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...'));
|
this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...'));
|
||||||
|
|
||||||
// Only send the complete recipe to save if not in download-only mode
|
// Only send the complete recipe to save if not in download-only mode
|
||||||
if (!isDownloadOnly) {
|
if (!isDownloadOnly) {
|
||||||
// Create FormData object for saving recipe
|
// Create FormData object for saving recipe
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
// Add image data - depends on import mode
|
// Add image data - depends on import mode
|
||||||
if (this.importManager.recipeImage) {
|
if (this.importManager.recipeImage) {
|
||||||
// Direct upload
|
// Direct upload
|
||||||
@@ -45,10 +45,10 @@ export class DownloadManager {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('No image data available');
|
throw new Error('No image data available');
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append('name', this.importManager.recipeName);
|
formData.append('name', this.importManager.recipeName);
|
||||||
formData.append('tags', JSON.stringify(this.importManager.recipeTags));
|
formData.append('tags', JSON.stringify(this.importManager.recipeTags));
|
||||||
|
|
||||||
// Prepare complete metadata including generation parameters
|
// Prepare complete metadata including generation parameters
|
||||||
const completeMetadata = {
|
const completeMetadata = {
|
||||||
base_model: this.importManager.recipeData.base_model || "",
|
base_model: this.importManager.recipeData.base_model || "",
|
||||||
@@ -65,7 +65,11 @@ export class DownloadManager {
|
|||||||
if (checkpointMetadata && typeof checkpointMetadata === 'object') {
|
if (checkpointMetadata && typeof checkpointMetadata === 'object') {
|
||||||
completeMetadata.checkpoint = checkpointMetadata;
|
completeMetadata.checkpoint = checkpointMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.importManager.recipeData && this.importManager.recipeData.extension) {
|
||||||
|
formData.append('extension', this.importManager.recipeData.extension);
|
||||||
|
}
|
||||||
|
|
||||||
// Add source_path to metadata to track where the recipe was imported from
|
// Add source_path to metadata to track where the recipe was imported from
|
||||||
if (this.importManager.importMode === 'url') {
|
if (this.importManager.importMode === 'url') {
|
||||||
const urlInput = document.getElementById('imageUrlInput');
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
@@ -73,15 +77,15 @@ export class DownloadManager {
|
|||||||
completeMetadata.source_path = urlInput.value;
|
completeMetadata.source_path = urlInput.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append('metadata', JSON.stringify(completeMetadata));
|
formData.append('metadata', JSON.stringify(completeMetadata));
|
||||||
|
|
||||||
// Send save request
|
// Send save request
|
||||||
const response = await fetch('/api/lm/recipes/save', {
|
const response = await fetch('/api/lm/recipes/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -102,19 +106,19 @@ export class DownloadManager {
|
|||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
if (isDownloadOnly) {
|
if (isDownloadOnly) {
|
||||||
if (failedDownloads === 0) {
|
if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.downloadSuccessful', {}, 'success');
|
showToast('toast.loras.downloadSuccessful', {}, 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.recipes.nameSaved', { name: this.importManager.recipeName }, 'success');
|
showToast('toast.recipes.nameSaved', { name: this.importManager.recipeName }, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal
|
// Close modal
|
||||||
modalManager.closeModal('importModal');
|
modalManager.closeModal('importModal');
|
||||||
|
|
||||||
// Refresh the recipe
|
// Refresh the recipe
|
||||||
window.recipeManager.loadRecipes();
|
window.recipeManager.loadRecipes();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
showToast('toast.recipes.processingError', { message: error.message }, 'error');
|
showToast('toast.recipes.processingError', { message: error.message }, 'error');
|
||||||
@@ -129,49 +133,49 @@ export class DownloadManager {
|
|||||||
if (!loraRoot) {
|
if (!loraRoot) {
|
||||||
throw new Error(translate('recipes.controls.import.errors.selectLoraRoot', {}, 'Please select a LoRA root directory'));
|
throw new Error(translate('recipes.controls.import.errors.selectLoraRoot', {}, 'Please select a LoRA root directory'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build target path
|
// Build target path
|
||||||
let targetPath = '';
|
let targetPath = '';
|
||||||
if (this.importManager.selectedFolder) {
|
if (this.importManager.selectedFolder) {
|
||||||
targetPath = this.importManager.selectedFolder;
|
targetPath = this.importManager.selectedFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique ID for this batch download
|
// Generate a unique ID for this batch download
|
||||||
const batchDownloadId = Date.now().toString();
|
const batchDownloadId = Date.now().toString();
|
||||||
|
|
||||||
// Set up WebSocket for progress updates
|
// Set up WebSocket for progress updates
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${batchDownloadId}`);
|
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${batchDownloadId}`);
|
||||||
|
|
||||||
// Show enhanced loading with progress details for multiple items
|
// Show enhanced loading with progress details for multiple items
|
||||||
const updateProgress = this.importManager.loadingManager.showDownloadProgress(
|
const updateProgress = this.importManager.loadingManager.showDownloadProgress(
|
||||||
this.importManager.downloadableLoRAs.length
|
this.importManager.downloadableLoRAs.length
|
||||||
);
|
);
|
||||||
|
|
||||||
let completedDownloads = 0;
|
let completedDownloads = 0;
|
||||||
let failedDownloads = 0;
|
let failedDownloads = 0;
|
||||||
let accessFailures = 0;
|
let accessFailures = 0;
|
||||||
let currentLoraProgress = 0;
|
let currentLoraProgress = 0;
|
||||||
|
|
||||||
// Set up progress tracking for current download
|
// Set up progress tracking for current download
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
// Handle download ID confirmation
|
// Handle download ID confirmation
|
||||||
if (data.type === 'download_id') {
|
if (data.type === 'download_id') {
|
||||||
console.log(`Connected to batch download progress with ID: ${data.download_id}`);
|
console.log(`Connected to batch download progress with ID: ${data.download_id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process progress updates for our current active download
|
// Process progress updates for our current active download
|
||||||
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) {
|
||||||
// Update current LoRA progress
|
// Update current LoRA progress
|
||||||
currentLoraProgress = data.progress;
|
currentLoraProgress = data.progress;
|
||||||
|
|
||||||
// Get current LoRA name
|
// Get current LoRA name
|
||||||
const currentLora = this.importManager.downloadableLoRAs[completedDownloads + failedDownloads];
|
const currentLora = this.importManager.downloadableLoRAs[completedDownloads + failedDownloads];
|
||||||
const loraName = currentLora ? currentLora.name : '';
|
const loraName = currentLora ? currentLora.name : '';
|
||||||
|
|
||||||
// Update progress display
|
// Update progress display
|
||||||
const metrics = {
|
const metrics = {
|
||||||
bytesDownloaded: data.bytes_downloaded,
|
bytesDownloaded: data.bytes_downloaded,
|
||||||
@@ -180,7 +184,7 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateProgress(currentLoraProgress, completedDownloads, loraName, metrics);
|
updateProgress(currentLoraProgress, completedDownloads, loraName, metrics);
|
||||||
|
|
||||||
// Add more detailed status messages based on progress
|
// Add more detailed status messages based on progress
|
||||||
if (currentLoraProgress < 3) {
|
if (currentLoraProgress < 3) {
|
||||||
this.importManager.loadingManager.setStatus(
|
this.importManager.loadingManager.setStatus(
|
||||||
@@ -203,17 +207,17 @@ export class DownloadManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useDefaultPaths = getStorageItem('use_default_path_loras', false);
|
const useDefaultPaths = getStorageItem('use_default_path_loras', false);
|
||||||
|
|
||||||
for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) {
|
for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) {
|
||||||
const lora = this.importManager.downloadableLoRAs[i];
|
const lora = this.importManager.downloadableLoRAs[i];
|
||||||
|
|
||||||
// Reset current LoRA progress for new download
|
// Reset current LoRA progress for new download
|
||||||
currentLoraProgress = 0;
|
currentLoraProgress = 0;
|
||||||
|
|
||||||
// Initial status update for new LoRA
|
// Initial status update for new LoRA
|
||||||
this.importManager.loadingManager.setStatus(translate('recipes.controls.import.startingDownload', { current: i+1, total: this.importManager.downloadableLoRAs.length }, `Starting download for LoRA ${i+1}/${this.importManager.downloadableLoRAs.length}`));
|
this.importManager.loadingManager.setStatus(translate('recipes.controls.import.startingDownload', { current: i + 1, total: this.importManager.downloadableLoRAs.length }, `Starting download for LoRA ${i + 1}/${this.importManager.downloadableLoRAs.length}`));
|
||||||
updateProgress(0, completedDownloads, lora.name);
|
updateProgress(0, completedDownloads, lora.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download the LoRA with download ID
|
// Download the LoRA with download ID
|
||||||
const response = await getModelApiClient(MODEL_TYPES.LORA).downloadModel(
|
const response = await getModelApiClient(MODEL_TYPES.LORA).downloadModel(
|
||||||
@@ -224,7 +228,7 @@ export class DownloadManager {
|
|||||||
useDefaultPaths,
|
useDefaultPaths,
|
||||||
batchDownloadId
|
batchDownloadId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
console.error(`Failed to download LoRA ${lora.name}: ${response.error}`);
|
console.error(`Failed to download LoRA ${lora.name}: ${response.error}`);
|
||||||
|
|
||||||
@@ -248,28 +252,28 @@ export class DownloadManager {
|
|||||||
// Continue with next download
|
// Continue with next download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close WebSocket
|
// Close WebSocket
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|
||||||
// Show appropriate completion message based on results
|
// Show appropriate completion message based on results
|
||||||
if (failedDownloads === 0) {
|
if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||||
} else {
|
} else {
|
||||||
if (accessFailures > 0) {
|
if (accessFailures > 0) {
|
||||||
showToast('toast.loras.downloadPartialWithAccess', {
|
showToast('toast.loras.downloadPartialWithAccess', {
|
||||||
completed: completedDownloads,
|
completed: completedDownloads,
|
||||||
total: this.importManager.downloadableLoRAs.length,
|
total: this.importManager.downloadableLoRAs.length,
|
||||||
accessFailures: accessFailures
|
accessFailures: accessFailures
|
||||||
}, 'error');
|
}, 'error');
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.loras.downloadPartialSuccess', {
|
showToast('toast.loras.downloadPartialSuccess', {
|
||||||
completed: completedDownloads,
|
completed: completedDownloads,
|
||||||
total: this.importManager.downloadableLoRAs.length
|
total: this.importManager.downloadableLoRAs.length
|
||||||
}, 'error');
|
}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return failedDownloads;
|
return failedDownloads;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ export class RecipeDataManager {
|
|||||||
|
|
||||||
showRecipeDetailsStep() {
|
showRecipeDetailsStep() {
|
||||||
this.importManager.stepManager.showStep('detailsStep');
|
this.importManager.stepManager.showStep('detailsStep');
|
||||||
|
|
||||||
// Set default recipe name from prompt or image filename
|
// Set default recipe name from prompt or image filename
|
||||||
const recipeName = document.getElementById('recipeName');
|
const recipeName = document.getElementById('recipeName');
|
||||||
|
|
||||||
// Check if we have recipe metadata from a shared recipe
|
// Check if we have recipe metadata from a shared recipe
|
||||||
if (this.importManager.recipeData && this.importManager.recipeData.from_recipe_metadata) {
|
if (this.importManager.recipeData && this.importManager.recipeData.from_recipe_metadata) {
|
||||||
// Use title from recipe metadata
|
// Use title from recipe metadata
|
||||||
@@ -19,24 +19,24 @@ export class RecipeDataManager {
|
|||||||
recipeName.value = this.importManager.recipeData.title;
|
recipeName.value = this.importManager.recipeData.title;
|
||||||
this.importManager.recipeName = this.importManager.recipeData.title;
|
this.importManager.recipeName = this.importManager.recipeData.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use tags from recipe metadata
|
// Use tags from recipe metadata
|
||||||
if (this.importManager.recipeData.tags && Array.isArray(this.importManager.recipeData.tags)) {
|
if (this.importManager.recipeData.tags && Array.isArray(this.importManager.recipeData.tags)) {
|
||||||
this.importManager.recipeTags = [...this.importManager.recipeData.tags];
|
this.importManager.recipeTags = [...this.importManager.recipeData.tags];
|
||||||
this.updateTagsDisplay();
|
this.updateTagsDisplay();
|
||||||
}
|
}
|
||||||
} else if (this.importManager.recipeData &&
|
} else if (this.importManager.recipeData &&
|
||||||
this.importManager.recipeData.gen_params &&
|
this.importManager.recipeData.gen_params &&
|
||||||
this.importManager.recipeData.gen_params.prompt) {
|
this.importManager.recipeData.gen_params.prompt) {
|
||||||
// Use the first 10 words from the prompt as the default recipe name
|
// Use the first 10 words from the prompt as the default recipe name
|
||||||
const promptWords = this.importManager.recipeData.gen_params.prompt.split(' ');
|
const promptWords = this.importManager.recipeData.gen_params.prompt.split(' ');
|
||||||
const truncatedPrompt = promptWords.slice(0, 10).join(' ');
|
const truncatedPrompt = promptWords.slice(0, 10).join(' ');
|
||||||
recipeName.value = truncatedPrompt;
|
recipeName.value = truncatedPrompt;
|
||||||
this.importManager.recipeName = truncatedPrompt;
|
this.importManager.recipeName = truncatedPrompt;
|
||||||
|
|
||||||
// Set up click handler to select all text for easy editing
|
// Set up click handler to select all text for easy editing
|
||||||
if (!recipeName.hasSelectAllHandler) {
|
if (!recipeName.hasSelectAllHandler) {
|
||||||
recipeName.addEventListener('click', function() {
|
recipeName.addEventListener('click', function () {
|
||||||
this.select();
|
this.select();
|
||||||
});
|
});
|
||||||
recipeName.hasSelectAllHandler = true;
|
recipeName.hasSelectAllHandler = true;
|
||||||
@@ -47,15 +47,15 @@ export class RecipeDataManager {
|
|||||||
recipeName.value = fileName;
|
recipeName.value = fileName;
|
||||||
this.importManager.recipeName = fileName;
|
this.importManager.recipeName = fileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always set up click handler for easy editing if not already set
|
// Always set up click handler for easy editing if not already set
|
||||||
if (!recipeName.hasSelectAllHandler) {
|
if (!recipeName.hasSelectAllHandler) {
|
||||||
recipeName.addEventListener('click', function() {
|
recipeName.addEventListener('click', function () {
|
||||||
this.select();
|
this.select();
|
||||||
});
|
});
|
||||||
recipeName.hasSelectAllHandler = true;
|
recipeName.hasSelectAllHandler = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
if (imagePreview) {
|
||||||
@@ -67,13 +67,24 @@ export class RecipeDataManager {
|
|||||||
};
|
};
|
||||||
reader.readAsDataURL(this.importManager.recipeImage);
|
reader.readAsDataURL(this.importManager.recipeImage);
|
||||||
} else if (this.importManager.recipeData && this.importManager.recipeData.image_base64) {
|
} else if (this.importManager.recipeData && this.importManager.recipeData.image_base64) {
|
||||||
// For URL mode - use the base64 image data returned from the backend
|
// For URL mode - use the base64 data returned from the backend
|
||||||
imagePreview.innerHTML = `<img src="data:image/jpeg;base64,${this.importManager.recipeData.image_base64}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}">`;
|
if (this.importManager.recipeData.is_video) {
|
||||||
|
const mimeType = this.importManager.recipeData.extension === '.webm' ? 'video/webm' : 'video/mp4';
|
||||||
|
imagePreview.innerHTML = `<video src="data:${mimeType};base64,${this.importManager.recipeData.image_base64}" controls autoplay loop muted class="recipe-preview-video"></video>`;
|
||||||
|
} else {
|
||||||
|
imagePreview.innerHTML = `<img src="data:image/jpeg;base64,${this.importManager.recipeData.image_base64}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}">`;
|
||||||
|
}
|
||||||
} else if (this.importManager.importMode === 'url') {
|
} else if (this.importManager.importMode === 'url') {
|
||||||
// Fallback for URL mode if no base64 data
|
// Fallback for URL mode if no base64 data
|
||||||
const urlInput = document.getElementById('imageUrlInput');
|
const urlInput = document.getElementById('imageUrlInput');
|
||||||
if (urlInput && urlInput.value) {
|
if (urlInput && urlInput.value) {
|
||||||
imagePreview.innerHTML = `<img src="${urlInput.value}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}" crossorigin="anonymous">`;
|
const url = urlInput.value.toLowerCase();
|
||||||
|
if (url.endsWith('.mp4') || url.endsWith('.webm')) {
|
||||||
|
const mimeType = url.endsWith('.webm') ? 'video/webm' : 'video/mp4';
|
||||||
|
imagePreview.innerHTML = `<video src="${urlInput.value}" controls autoplay loop muted class="recipe-preview-video"></video>`;
|
||||||
|
} else {
|
||||||
|
imagePreview.innerHTML = `<img src="${urlInput.value}" alt="${translate('recipes.controls.import.recipePreviewAlt', {}, 'Recipe preview')}" crossorigin="anonymous">`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +96,7 @@ export class RecipeDataManager {
|
|||||||
if (loraCountInfo) {
|
if (loraCountInfo) {
|
||||||
loraCountInfo.textContent = translate('recipes.controls.import.loraCountInfo', { existing: existingLoras, total: totalLoras }, `(${existingLoras}/${totalLoras} in library)`);
|
loraCountInfo.textContent = translate('recipes.controls.import.loraCountInfo', { existing: existingLoras, total: totalLoras }, `(${existingLoras}/${totalLoras} in library)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display LoRAs list
|
// Display LoRAs list
|
||||||
const lorasList = document.getElementById('lorasList');
|
const lorasList = document.getElementById('lorasList');
|
||||||
if (lorasList) {
|
if (lorasList) {
|
||||||
@@ -94,7 +105,7 @@ export class RecipeDataManager {
|
|||||||
const isDeleted = lora.isDeleted;
|
const isDeleted = lora.isDeleted;
|
||||||
const isEarlyAccess = lora.isEarlyAccess;
|
const isEarlyAccess = lora.isEarlyAccess;
|
||||||
const localPath = lora.localPath || '';
|
const localPath = lora.localPath || '';
|
||||||
|
|
||||||
// Create status badge based on LoRA status
|
// Create status badge based on LoRA status
|
||||||
let statusBadge;
|
let statusBadge;
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
@@ -102,7 +113,7 @@ export class RecipeDataManager {
|
|||||||
<i class="fas fa-exclamation-circle"></i> ${translate('recipes.controls.import.deletedFromCivitai', {}, 'Deleted from Civitai')}
|
<i class="fas fa-exclamation-circle"></i> ${translate('recipes.controls.import.deletedFromCivitai', {}, 'Deleted from Civitai')}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
statusBadge = existsLocally ?
|
statusBadge = existsLocally ?
|
||||||
`<div class="local-badge">
|
`<div class="local-badge">
|
||||||
<i class="fas fa-check"></i> ${translate('recipes.controls.import.inLibrary', {}, 'In Library')}
|
<i class="fas fa-check"></i> ${translate('recipes.controls.import.inLibrary', {}, 'In Library')}
|
||||||
<div class="local-path">${localPath}</div>
|
<div class="local-path">${localPath}</div>
|
||||||
@@ -126,7 +137,7 @@ export class RecipeDataManager {
|
|||||||
console.warn('Failed to format early access date', e);
|
console.warn('Failed to format early access date', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
earlyAccessBadge = `<div class="early-access-badge">
|
earlyAccessBadge = `<div class="early-access-badge">
|
||||||
<i class="fas fa-clock"></i> ${translate('recipes.controls.import.earlyAccess', {}, 'Early Access')}
|
<i class="fas fa-clock"></i> ${translate('recipes.controls.import.earlyAccess', {}, 'Early Access')}
|
||||||
<div class="early-access-info">${earlyAccessInfo} ${translate('recipes.controls.import.verifyEarlyAccess', {}, 'Verify that you have purchased early access before downloading.')}</div>
|
<div class="early-access-info">${earlyAccessInfo} ${translate('recipes.controls.import.verifyEarlyAccess', {}, 'Verify that you have purchased early access before downloading.')}</div>
|
||||||
@@ -134,7 +145,7 @@ export class RecipeDataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Format size if available
|
// Format size if available
|
||||||
const sizeDisplay = lora.size ?
|
const sizeDisplay = lora.size ?
|
||||||
`<div class="size-badge">${this.importManager.formatFileSize(lora.size)}</div>` : '';
|
`<div class="size-badge">${this.importManager.formatFileSize(lora.size)}</div>` : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -161,9 +172,9 @@ export class RecipeDataManager {
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for early access loras and show warning if any exist
|
// Check for early access loras and show warning if any exist
|
||||||
const earlyAccessLoras = this.importManager.recipeData.loras.filter(lora =>
|
const earlyAccessLoras = this.importManager.recipeData.loras.filter(lora =>
|
||||||
lora.isEarlyAccess && !lora.existsLocally && !lora.isDeleted);
|
lora.isEarlyAccess && !lora.existsLocally && !lora.isDeleted);
|
||||||
if (earlyAccessLoras.length > 0) {
|
if (earlyAccessLoras.length > 0) {
|
||||||
// Show a warning about early access loras
|
// Show a warning about early access loras
|
||||||
@@ -179,7 +190,7 @@ export class RecipeDataManager {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Show the warning message
|
// Show the warning message
|
||||||
const buttonsContainer = document.querySelector('#detailsStep .modal-actions');
|
const buttonsContainer = document.querySelector('#detailsStep .modal-actions');
|
||||||
if (buttonsContainer) {
|
if (buttonsContainer) {
|
||||||
@@ -188,7 +199,7 @@ export class RecipeDataManager {
|
|||||||
if (existingWarning) {
|
if (existingWarning) {
|
||||||
existingWarning.remove();
|
existingWarning.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new warning
|
// Add new warning
|
||||||
const warningContainer = document.createElement('div');
|
const warningContainer = document.createElement('div');
|
||||||
warningContainer.id = 'earlyAccessWarning';
|
warningContainer.id = 'earlyAccessWarning';
|
||||||
@@ -196,27 +207,27 @@ export class RecipeDataManager {
|
|||||||
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate recipes and display warning if found
|
// Check for duplicate recipes and display warning if found
|
||||||
this.checkAndDisplayDuplicates();
|
this.checkAndDisplayDuplicates();
|
||||||
|
|
||||||
// Update Next button state based on missing LoRAs and duplicates
|
// Update Next button state based on missing LoRAs and duplicates
|
||||||
this.updateNextButtonState();
|
this.updateNextButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkAndDisplayDuplicates() {
|
checkAndDisplayDuplicates() {
|
||||||
// Check if we have duplicate recipes
|
// Check if we have duplicate recipes
|
||||||
if (this.importManager.recipeData &&
|
if (this.importManager.recipeData &&
|
||||||
this.importManager.recipeData.matching_recipes &&
|
this.importManager.recipeData.matching_recipes &&
|
||||||
this.importManager.recipeData.matching_recipes.length > 0) {
|
this.importManager.recipeData.matching_recipes.length > 0) {
|
||||||
|
|
||||||
// Store duplicates in the importManager for later use
|
// Store duplicates in the importManager for later use
|
||||||
this.importManager.duplicateRecipes = this.importManager.recipeData.matching_recipes;
|
this.importManager.duplicateRecipes = this.importManager.recipeData.matching_recipes;
|
||||||
|
|
||||||
// Create duplicate warning container
|
// Create duplicate warning container
|
||||||
const duplicateContainer = document.getElementById('duplicateRecipesContainer') ||
|
const duplicateContainer = document.getElementById('duplicateRecipesContainer') ||
|
||||||
this.createDuplicateContainer();
|
this.createDuplicateContainer();
|
||||||
|
|
||||||
// Format date helper function
|
// Format date helper function
|
||||||
const formatDate = (timestamp) => {
|
const formatDate = (timestamp) => {
|
||||||
try {
|
try {
|
||||||
@@ -226,7 +237,7 @@ export class RecipeDataManager {
|
|||||||
return 'Unknown date';
|
return 'Unknown date';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate the HTML for duplicate recipes warning
|
// Generate the HTML for duplicate recipes warning
|
||||||
duplicateContainer.innerHTML = `
|
duplicateContainer.innerHTML = `
|
||||||
<div class="duplicate-warning">
|
<div class="duplicate-warning">
|
||||||
@@ -262,10 +273,10 @@ export class RecipeDataManager {
|
|||||||
`).join('')}
|
`).join('')}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Show the duplicate container
|
// Show the duplicate container
|
||||||
duplicateContainer.style.display = 'block';
|
duplicateContainer.style.display = 'block';
|
||||||
|
|
||||||
// Add click event for the toggle button
|
// Add click event for the toggle button
|
||||||
const toggleButton = document.getElementById('toggleDuplicatesList');
|
const toggleButton = document.getElementById('toggleDuplicatesList');
|
||||||
if (toggleButton) {
|
if (toggleButton) {
|
||||||
@@ -290,49 +301,49 @@ export class RecipeDataManager {
|
|||||||
if (duplicateContainer) {
|
if (duplicateContainer) {
|
||||||
duplicateContainer.style.display = 'none';
|
duplicateContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset duplicate tracking
|
// Reset duplicate tracking
|
||||||
this.importManager.duplicateRecipes = [];
|
this.importManager.duplicateRecipes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createDuplicateContainer() {
|
createDuplicateContainer() {
|
||||||
// Find where to insert the duplicate container
|
// Find where to insert the duplicate container
|
||||||
const lorasListContainer = document.querySelector('.input-group:has(#lorasList)');
|
const lorasListContainer = document.querySelector('.input-group:has(#lorasList)');
|
||||||
|
|
||||||
if (!lorasListContainer) return null;
|
if (!lorasListContainer) return null;
|
||||||
|
|
||||||
// Create container
|
// Create container
|
||||||
const duplicateContainer = document.createElement('div');
|
const duplicateContainer = document.createElement('div');
|
||||||
duplicateContainer.id = 'duplicateRecipesContainer';
|
duplicateContainer.id = 'duplicateRecipesContainer';
|
||||||
duplicateContainer.className = 'duplicate-recipes-container';
|
duplicateContainer.className = 'duplicate-recipes-container';
|
||||||
|
|
||||||
// Insert before the LoRA list
|
// Insert before the LoRA list
|
||||||
lorasListContainer.parentNode.insertBefore(duplicateContainer, lorasListContainer);
|
lorasListContainer.parentNode.insertBefore(duplicateContainer, lorasListContainer);
|
||||||
|
|
||||||
return duplicateContainer;
|
return duplicateContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNextButtonState() {
|
updateNextButtonState() {
|
||||||
const nextButton = document.querySelector('#detailsStep .primary-btn');
|
const nextButton = document.querySelector('#detailsStep .primary-btn');
|
||||||
const actionsContainer = document.querySelector('#detailsStep .modal-actions');
|
const actionsContainer = document.querySelector('#detailsStep .modal-actions');
|
||||||
if (!nextButton || !actionsContainer) return;
|
if (!nextButton || !actionsContainer) return;
|
||||||
|
|
||||||
// Always clean up previous warnings and buttons first
|
// Always clean up previous warnings and buttons first
|
||||||
const existingWarning = document.getElementById('deletedLorasWarning');
|
const existingWarning = document.getElementById('deletedLorasWarning');
|
||||||
if (existingWarning) {
|
if (existingWarning) {
|
||||||
existingWarning.remove();
|
existingWarning.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any existing "import anyway" button
|
// Remove any existing "import anyway" button
|
||||||
const importAnywayBtn = document.getElementById('importAnywayBtn');
|
const importAnywayBtn = document.getElementById('importAnywayBtn');
|
||||||
if (importAnywayBtn) {
|
if (importAnywayBtn) {
|
||||||
importAnywayBtn.remove();
|
importAnywayBtn.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count deleted LoRAs
|
// Count deleted LoRAs
|
||||||
const deletedLoras = this.importManager.recipeData.loras.filter(lora => lora.isDeleted).length;
|
const deletedLoras = this.importManager.recipeData.loras.filter(lora => lora.isDeleted).length;
|
||||||
|
|
||||||
// If we have deleted LoRAs, show a warning
|
// If we have deleted LoRAs, show a warning
|
||||||
if (deletedLoras > 0) {
|
if (deletedLoras > 0) {
|
||||||
// Create a new warning container above the buttons
|
// Create a new warning container above the buttons
|
||||||
@@ -340,7 +351,7 @@ export class RecipeDataManager {
|
|||||||
const warningContainer = document.createElement('div');
|
const warningContainer = document.createElement('div');
|
||||||
warningContainer.id = 'deletedLorasWarning';
|
warningContainer.id = 'deletedLorasWarning';
|
||||||
warningContainer.className = 'deleted-loras-warning';
|
warningContainer.className = 'deleted-loras-warning';
|
||||||
|
|
||||||
// Create warning message
|
// Create warning message
|
||||||
warningContainer.innerHTML = `
|
warningContainer.innerHTML = `
|
||||||
<div class="warning-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
<div class="warning-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||||
@@ -349,19 +360,19 @@ export class RecipeDataManager {
|
|||||||
<div class="warning-text">These LoRAs cannot be downloaded. If you continue, they will remain in the recipe but won't be included when used.</div>
|
<div class="warning-text">These LoRAs cannot be downloaded. If you continue, they will remain in the recipe but won't be included when used.</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Insert before the buttons container
|
// Insert before the buttons container
|
||||||
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates but don't change button actions
|
// Check for duplicates but don't change button actions
|
||||||
const missingNotDeleted = this.importManager.recipeData.loras.filter(
|
const missingNotDeleted = this.importManager.recipeData.loras.filter(
|
||||||
lora => !lora.existsLocally && !lora.isDeleted
|
lora => !lora.existsLocally && !lora.isDeleted
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
// Standard button behavior regardless of duplicates
|
// Standard button behavior regardless of duplicates
|
||||||
nextButton.classList.remove('warning-btn');
|
nextButton.classList.remove('warning-btn');
|
||||||
|
|
||||||
if (missingNotDeleted > 0) {
|
if (missingNotDeleted > 0) {
|
||||||
nextButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
nextButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
||||||
} else {
|
} else {
|
||||||
@@ -372,30 +383,30 @@ export class RecipeDataManager {
|
|||||||
addTag() {
|
addTag() {
|
||||||
const tagInput = document.getElementById('tagInput');
|
const tagInput = document.getElementById('tagInput');
|
||||||
const tag = tagInput.value.trim();
|
const tag = tagInput.value.trim();
|
||||||
|
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
if (!this.importManager.recipeTags.includes(tag)) {
|
if (!this.importManager.recipeTags.includes(tag)) {
|
||||||
this.importManager.recipeTags.push(tag);
|
this.importManager.recipeTags.push(tag);
|
||||||
this.updateTagsDisplay();
|
this.updateTagsDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
tagInput.value = '';
|
tagInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTag(tag) {
|
removeTag(tag) {
|
||||||
this.importManager.recipeTags = this.importManager.recipeTags.filter(t => t !== tag);
|
this.importManager.recipeTags = this.importManager.recipeTags.filter(t => t !== tag);
|
||||||
this.updateTagsDisplay();
|
this.updateTagsDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTagsDisplay() {
|
updateTagsDisplay() {
|
||||||
const tagsContainer = document.getElementById('tagsContainer');
|
const tagsContainer = document.getElementById('tagsContainer');
|
||||||
|
|
||||||
if (this.importManager.recipeTags.length === 0) {
|
if (this.importManager.recipeTags.length === 0) {
|
||||||
tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
|
tagsContainer.innerHTML = `<div class="empty-tags">${translate('recipes.controls.import.noTagsAdded', {}, 'No tags added')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tagsContainer.innerHTML = this.importManager.recipeTags.map(tag => `
|
tagsContainer.innerHTML = this.importManager.recipeTags.map(tag => `
|
||||||
<div class="recipe-tag">
|
<div class="recipe-tag">
|
||||||
${tag}
|
${tag}
|
||||||
@@ -410,7 +421,7 @@ export class RecipeDataManager {
|
|||||||
showToast('toast.recipes.enterRecipeName', {}, 'error');
|
showToast('toast.recipes.enterRecipeName', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically mark all deleted LoRAs as excluded
|
// Automatically mark all deleted LoRAs as excluded
|
||||||
if (this.importManager.recipeData && this.importManager.recipeData.loras) {
|
if (this.importManager.recipeData && this.importManager.recipeData.loras) {
|
||||||
this.importManager.recipeData.loras.forEach(lora => {
|
this.importManager.recipeData.loras.forEach(lora => {
|
||||||
@@ -419,11 +430,11 @@ export class RecipeDataManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update missing LoRAs list to exclude deleted LoRAs
|
// Update missing LoRAs list to exclude deleted LoRAs
|
||||||
this.importManager.missingLoras = this.importManager.recipeData.loras.filter(lora =>
|
this.importManager.missingLoras = this.importManager.recipeData.loras.filter(lora =>
|
||||||
!lora.existsLocally && !lora.isDeleted);
|
!lora.existsLocally && !lora.isDeleted);
|
||||||
|
|
||||||
// If we have downloadable missing LoRAs, go to location step
|
// If we have downloadable missing LoRAs, go to location step
|
||||||
if (this.importManager.missingLoras.length > 0) {
|
if (this.importManager.missingLoras.length > 0) {
|
||||||
// Store only downloadable LoRAs for the download step
|
// Store only downloadable LoRAs for the download step
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ async def test_analyze_remote_image_download_failure_cleans_temp(tmp_path, monke
|
|||||||
|
|
||||||
temp_path = tmp_path / "temp.jpg"
|
temp_path = tmp_path / "temp.jpg"
|
||||||
|
|
||||||
def create_temp_path():
|
def create_temp_path(suffix=".jpg"):
|
||||||
temp_path.write_bytes(b"")
|
temp_path.write_bytes(b"")
|
||||||
return str(temp_path)
|
return str(temp_path)
|
||||||
|
|
||||||
@@ -401,3 +401,55 @@ async def test_save_recipe_from_widget_allows_empty_lora(tmp_path):
|
|||||||
assert stored["loras"] == []
|
assert stored["loras"] == []
|
||||||
assert stored["title"] == "recipe"
|
assert stored["title"] == "recipe"
|
||||||
assert scanner.added and scanner.added[0]["loras"] == []
|
assert scanner.added and scanner.added[0]["loras"] == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_analyze_remote_video(tmp_path):
|
||||||
|
exif_utils = DummyExifUtils()
|
||||||
|
|
||||||
|
class DummyFactory:
|
||||||
|
def create_parser(self, metadata):
|
||||||
|
async def parse_metadata(m, recipe_scanner):
|
||||||
|
return {"loras": []}
|
||||||
|
return SimpleNamespace(parse_metadata=parse_metadata)
|
||||||
|
|
||||||
|
async def downloader_factory():
|
||||||
|
class Downloader:
|
||||||
|
async def download_file(self, url, path, use_auth=False):
|
||||||
|
Path(path).write_bytes(b"video-content")
|
||||||
|
return True, "success"
|
||||||
|
|
||||||
|
return Downloader()
|
||||||
|
|
||||||
|
service = RecipeAnalysisService(
|
||||||
|
exif_utils=exif_utils,
|
||||||
|
recipe_parser_factory=DummyFactory(),
|
||||||
|
downloader_factory=downloader_factory,
|
||||||
|
metadata_collector=None,
|
||||||
|
metadata_processor_cls=None,
|
||||||
|
metadata_registry_cls=None,
|
||||||
|
standalone_mode=False,
|
||||||
|
logger=logging.getLogger("test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DummyClient:
|
||||||
|
async def get_image_info(self, image_id):
|
||||||
|
return {
|
||||||
|
"url": "https://civitai.com/video.mp4",
|
||||||
|
"type": "video",
|
||||||
|
"meta": {"prompt": "video prompt"},
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyScanner:
|
||||||
|
async def find_recipes_by_fingerprint(self, fingerprint):
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = await service.analyze_remote_image(
|
||||||
|
url="https://civitai.com/images/123",
|
||||||
|
recipe_scanner=DummyScanner(),
|
||||||
|
civitai_client=DummyClient(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.payload["is_video"] is True
|
||||||
|
assert result.payload["extension"] == ".mp4"
|
||||||
|
assert result.payload["image_base64"] is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user