mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
Add recipe syntax endpoint and update RecipeCard and RecipeModal for syntax fetching
- Introduced a new API endpoint to retrieve recipe syntax for LoRAs, allowing for better integration with the frontend. - Updated RecipeCard to fetch recipe syntax from the backend instead of generating it locally. - Modified RecipeModal to store the recipe ID and fetch syntax when the copy button is clicked, improving user experience. - Enhanced error handling for fetching recipe syntax to provide clearer feedback to users.
This commit is contained in:
@@ -47,6 +47,9 @@ class RecipeRoutes:
|
|||||||
app.router.add_get('/api/recipe/{recipe_id}/share', routes.share_recipe)
|
app.router.add_get('/api/recipe/{recipe_id}/share', routes.share_recipe)
|
||||||
app.router.add_get('/api/recipe/{recipe_id}/share/download', routes.download_shared_recipe)
|
app.router.add_get('/api/recipe/{recipe_id}/share/download', routes.download_shared_recipe)
|
||||||
|
|
||||||
|
# Add new endpoint for getting recipe syntax
|
||||||
|
app.router.add_get('/api/recipe/{recipe_id}/syntax', routes.get_recipe_syntax)
|
||||||
|
|
||||||
# Start cache initialization
|
# Start cache initialization
|
||||||
app.on_startup.append(routes._init_cache)
|
app.on_startup.append(routes._init_cache)
|
||||||
|
|
||||||
@@ -432,19 +435,20 @@ class RecipeRoutes:
|
|||||||
# Format loras data according to the recipe.json format
|
# Format loras data according to the recipe.json format
|
||||||
loras_data = []
|
loras_data = []
|
||||||
for lora in metadata.get("loras", []):
|
for lora in metadata.get("loras", []):
|
||||||
# Skip deleted LoRAs if they're marked to be excluded
|
# Modified: Always include deleted LoRAs in the recipe metadata
|
||||||
if lora.get("isDeleted", False) and lora.get("exclude", False):
|
# Even if they're marked to be excluded, we still keep their identifying information
|
||||||
continue
|
# The exclude flag will only be used to determine if they should be included in recipe syntax
|
||||||
|
|
||||||
# Convert frontend lora format to recipe format
|
# Convert frontend lora format to recipe format
|
||||||
lora_entry = {
|
lora_entry = {
|
||||||
"file_name": lora.get("file_name", "") or os.path.splitext(os.path.basename(lora.get("localPath", "")))[0],
|
"file_name": lora.get("file_name", "") or os.path.splitext(os.path.basename(lora.get("localPath", "")))[0] if lora.get("localPath") else "",
|
||||||
"hash": lora.get("hash", "").lower() if lora.get("hash") else "",
|
"hash": lora.get("hash", "").lower() if lora.get("hash") else "",
|
||||||
"strength": float(lora.get("weight", 1.0)),
|
"strength": float(lora.get("weight", 1.0)),
|
||||||
"modelVersionId": lora.get("id", ""),
|
"modelVersionId": lora.get("id", ""),
|
||||||
"modelName": lora.get("name", ""),
|
"modelName": lora.get("name", ""),
|
||||||
"modelVersionName": lora.get("version", ""),
|
"modelVersionName": lora.get("version", ""),
|
||||||
"isDeleted": lora.get("isDeleted", False) # Preserve deletion status in saved recipe
|
"isDeleted": lora.get("isDeleted", False), # Preserve deletion status in saved recipe
|
||||||
|
"exclude": lora.get("exclude", False) # Add exclude flag to the recipe
|
||||||
}
|
}
|
||||||
loras_data.append(lora_entry)
|
loras_data.append(lora_entry)
|
||||||
|
|
||||||
@@ -905,3 +909,78 @@ class RecipeRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving recipe from widget: {e}", exc_info=True)
|
logger.error(f"Error saving recipe from widget: {e}", exc_info=True)
|
||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
|
async def get_recipe_syntax(self, request: web.Request) -> web.Response:
|
||||||
|
"""Generate recipe syntax for LoRAs in the recipe, looking up proper file names using hash_index"""
|
||||||
|
try:
|
||||||
|
recipe_id = request.match_info['recipe_id']
|
||||||
|
|
||||||
|
# Get all recipes from cache
|
||||||
|
cache = await self.recipe_scanner.get_cached_data()
|
||||||
|
|
||||||
|
# Find the specific recipe
|
||||||
|
recipe = next((r for r in cache.raw_data if str(r.get('id', '')) == recipe_id), None)
|
||||||
|
|
||||||
|
if not recipe:
|
||||||
|
return web.json_response({"error": "Recipe not found"}, status=404)
|
||||||
|
|
||||||
|
# Get the loras from the recipe
|
||||||
|
loras = recipe.get('loras', [])
|
||||||
|
|
||||||
|
if not loras:
|
||||||
|
return web.json_response({"error": "No LoRAs found in this recipe"}, status=400)
|
||||||
|
|
||||||
|
# Generate recipe syntax for all LoRAs that:
|
||||||
|
# 1. Are in the library (not deleted) OR
|
||||||
|
# 2. Are deleted but not marked for exclusion
|
||||||
|
lora_syntax_parts = []
|
||||||
|
|
||||||
|
# Access the hash_index from lora_scanner
|
||||||
|
hash_index = self.recipe_scanner._lora_scanner._hash_index
|
||||||
|
|
||||||
|
for lora in loras:
|
||||||
|
# Skip loras that are deleted AND marked for exclusion
|
||||||
|
if lora.get("isDeleted", False) and lora.get("exclude", False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get the strength
|
||||||
|
strength = lora.get("strength", 1.0)
|
||||||
|
|
||||||
|
# Try to find the actual file name for this lora
|
||||||
|
file_name = None
|
||||||
|
hash_value = lora.get("hash", "").lower()
|
||||||
|
|
||||||
|
if hash_value and hasattr(hash_index, "_hash_to_path"):
|
||||||
|
# Look up the file path from the hash
|
||||||
|
file_path = hash_index._hash_to_path.get(hash_value)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
# Extract the file name without extension from the path
|
||||||
|
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
|
||||||
|
# If hash lookup failed, fall back to modelVersionId lookup
|
||||||
|
if not file_name and lora.get("modelVersionId"):
|
||||||
|
# Search for files with matching modelVersionId
|
||||||
|
all_loras = await self.recipe_scanner._lora_scanner.get_cached_data()
|
||||||
|
for cached_lora in all_loras.raw_data:
|
||||||
|
if "civitai" in cached_lora and cached_lora["civitai"].get("id") == lora.get("modelVersionId"):
|
||||||
|
file_name = os.path.splitext(os.path.basename(cached_lora["path"]))[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
# If all lookups failed, use the file_name from the recipe
|
||||||
|
if not file_name:
|
||||||
|
file_name = lora.get("file_name", "unknown-lora")
|
||||||
|
|
||||||
|
# Add to syntax parts
|
||||||
|
lora_syntax_parts.append(f"<lora:{file_name}:{strength}>")
|
||||||
|
|
||||||
|
# Join the LoRA syntax parts
|
||||||
|
lora_syntax = " ".join(lora_syntax_parts)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'syntax': lora_syntax
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error generating recipe syntax: {e}", exc_info=True)
|
||||||
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|||||||
@@ -96,22 +96,24 @@ class RecipeCard {
|
|||||||
|
|
||||||
copyRecipeSyntax() {
|
copyRecipeSyntax() {
|
||||||
try {
|
try {
|
||||||
// Generate recipe syntax in the format <lora:file_name:strength> separated by spaces
|
// Get recipe ID
|
||||||
const loras = this.recipe.loras || [];
|
const recipeId = this.recipe.id;
|
||||||
if (loras.length === 0) {
|
if (!recipeId) {
|
||||||
showToast('No LoRAs in this recipe to copy', 'warning');
|
showToast('Cannot copy recipe syntax: Missing recipe ID', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const syntax = loras.map(lora => {
|
// Fallback if button not found
|
||||||
// Use file_name if available, otherwise use empty placeholder
|
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||||
const fileName = lora.file_name || '[missing-lora]';
|
.then(response => response.json())
|
||||||
const strength = lora.strength || 1.0;
|
.then(data => {
|
||||||
return `<lora:${fileName}:${strength}>`;
|
if (data.success && data.syntax) {
|
||||||
}).join(' ');
|
return navigator.clipboard.writeText(data.syntax);
|
||||||
|
} else {
|
||||||
// Copy to clipboard
|
throw new Error(data.error || 'No syntax returned');
|
||||||
navigator.clipboard.writeText(syntax)
|
}
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
showToast('Recipe syntax copied to clipboard', 'success');
|
showToast('Recipe syntax copied to clipboard', 'success');
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class RecipeModal {
|
|||||||
modalTitle.textContent = recipe.title || 'Recipe Details';
|
modalTitle.textContent = recipe.title || 'Recipe Details';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the recipe ID for copy syntax API call
|
||||||
|
this.recipeId = recipe.id;
|
||||||
|
|
||||||
// Set recipe tags if they exist
|
// Set recipe tags if they exist
|
||||||
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
||||||
const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent');
|
const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent');
|
||||||
@@ -265,10 +268,8 @@ class RecipeModal {
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Generate recipe syntax for copy button
|
// Generate recipe syntax for copy button (this is now a placeholder, actual syntax will be fetched from the API)
|
||||||
this.recipeLorasSyntax = recipe.loras.map(lora =>
|
this.recipeLorasSyntax = '';
|
||||||
`<lora:${lora.file_name}:${lora.strength || 1.0}>`
|
|
||||||
).join(' ');
|
|
||||||
|
|
||||||
} else if (lorasListElement) {
|
} else if (lorasListElement) {
|
||||||
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
||||||
@@ -301,11 +302,42 @@ class RecipeModal {
|
|||||||
|
|
||||||
if (copyRecipeSyntaxBtn) {
|
if (copyRecipeSyntaxBtn) {
|
||||||
copyRecipeSyntaxBtn.addEventListener('click', () => {
|
copyRecipeSyntaxBtn.addEventListener('click', () => {
|
||||||
this.copyToClipboard(this.recipeLorasSyntax, 'Recipe syntax copied to clipboard');
|
// Use backend API to get recipe syntax
|
||||||
|
this.fetchAndCopyRecipeSyntax();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch recipe syntax from backend and copy to clipboard
|
||||||
|
async fetchAndCopyRecipeSyntax() {
|
||||||
|
if (!this.recipeId) {
|
||||||
|
showToast('No recipe ID available', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch recipe syntax from backend
|
||||||
|
const response = await fetch(`/api/recipe/${this.recipeId}/syntax`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get recipe syntax: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.syntax) {
|
||||||
|
// Copy to clipboard
|
||||||
|
await navigator.clipboard.writeText(data.syntax);
|
||||||
|
showToast('Recipe syntax copied to clipboard', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'No syntax returned from server');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching recipe syntax:', error);
|
||||||
|
showToast(`Error copying recipe syntax: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to copy text to clipboard
|
// Helper method to copy text to clipboard
|
||||||
copyToClipboard(text, successMessage) {
|
copyToClipboard(text, successMessage) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update LoRA count information
|
// Update LoRA count information
|
||||||
const totalLoras = this.recipeData.loras.length;
|
const totalLoras = this.recipeData.loras.length;
|
||||||
const existingLoras = this.recipeData.loras.filter(lora => lora.existsLocally).length;
|
const existingLoras = this.recipeData.loras.filter(lora => lora.existsLocally).length;
|
||||||
@@ -549,33 +549,24 @@ export class ImportManager {
|
|||||||
<div class="warning-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
<div class="warning-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||||
<div class="warning-content">
|
<div class="warning-content">
|
||||||
<div class="warning-title">${deletedLoras} LoRA(s) have been deleted from Civitai</div>
|
<div class="warning-title">${deletedLoras} LoRA(s) have been deleted from Civitai</div>
|
||||||
<div class="warning-text">These LoRAs cannot be downloaded. If you continue, they will be removed from the recipe.</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);
|
||||||
|
}
|
||||||
|
|
||||||
// Update next button text to be more clear
|
// If we have missing LoRAs (not deleted), show "Download Missing LoRAs"
|
||||||
nextButton.textContent = 'Continue Without Deleted LoRAs';
|
// Otherwise show "Save Recipe"
|
||||||
|
const missingNotDeleted = this.recipeData.loras.filter(
|
||||||
|
lora => !lora.existsLocally && !lora.isDeleted
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (missingNotDeleted > 0) {
|
||||||
|
nextButton.textContent = 'Download Missing LoRAs';
|
||||||
} else {
|
} else {
|
||||||
// Remove warning if no deleted LoRAs
|
nextButton.textContent = 'Save Recipe';
|
||||||
const warningMsg = document.getElementById('deletedLorasWarning');
|
|
||||||
if (warningMsg) {
|
|
||||||
warningMsg.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have missing LoRAs (not deleted), show "Download Missing LoRAs"
|
|
||||||
// Otherwise show "Save Recipe"
|
|
||||||
const missingNotDeleted = this.recipeData.loras.filter(
|
|
||||||
lora => !lora.existsLocally && !lora.isDeleted
|
|
||||||
).length;
|
|
||||||
|
|
||||||
if (missingNotDeleted > 0) {
|
|
||||||
nextButton.textContent = 'Download Missing LoRAs';
|
|
||||||
} else {
|
|
||||||
nextButton.textContent = 'Save Recipe';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user