mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -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/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
|
||||
app.on_startup.append(routes._init_cache)
|
||||
|
||||
@@ -432,19 +435,20 @@ class RecipeRoutes:
|
||||
# Format loras data according to the recipe.json format
|
||||
loras_data = []
|
||||
for lora in metadata.get("loras", []):
|
||||
# Skip deleted LoRAs if they're marked to be excluded
|
||||
if lora.get("isDeleted", False) and lora.get("exclude", False):
|
||||
continue
|
||||
# Modified: Always include deleted LoRAs in the recipe metadata
|
||||
# Even if they're marked to be excluded, we still keep their identifying information
|
||||
# The exclude flag will only be used to determine if they should be included in recipe syntax
|
||||
|
||||
# Convert frontend lora format to recipe format
|
||||
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 "",
|
||||
"strength": float(lora.get("weight", 1.0)),
|
||||
"modelVersionId": lora.get("id", ""),
|
||||
"modelName": lora.get("name", ""),
|
||||
"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)
|
||||
|
||||
@@ -905,3 +909,78 @@ class RecipeRoutes:
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving recipe from widget: {e}", exc_info=True)
|
||||
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() {
|
||||
try {
|
||||
// Generate recipe syntax in the format <lora:file_name:strength> separated by spaces
|
||||
const loras = this.recipe.loras || [];
|
||||
if (loras.length === 0) {
|
||||
showToast('No LoRAs in this recipe to copy', 'warning');
|
||||
// Get recipe ID
|
||||
const recipeId = this.recipe.id;
|
||||
if (!recipeId) {
|
||||
showToast('Cannot copy recipe syntax: Missing recipe ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const syntax = loras.map(lora => {
|
||||
// Use file_name if available, otherwise use empty placeholder
|
||||
const fileName = lora.file_name || '[missing-lora]';
|
||||
const strength = lora.strength || 1.0;
|
||||
return `<lora:${fileName}:${strength}>`;
|
||||
}).join(' ');
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard.writeText(syntax)
|
||||
// Fallback if button not found
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
return navigator.clipboard.writeText(data.syntax);
|
||||
} else {
|
||||
throw new Error(data.error || 'No syntax returned');
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
showToast('Recipe syntax copied to clipboard', 'success');
|
||||
})
|
||||
|
||||
@@ -41,6 +41,9 @@ class RecipeModal {
|
||||
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
|
||||
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
||||
const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent');
|
||||
@@ -265,10 +268,8 @@ class RecipeModal {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Generate recipe syntax for copy button
|
||||
this.recipeLorasSyntax = recipe.loras.map(lora =>
|
||||
`<lora:${lora.file_name}:${lora.strength || 1.0}>`
|
||||
).join(' ');
|
||||
// Generate recipe syntax for copy button (this is now a placeholder, actual syntax will be fetched from the API)
|
||||
this.recipeLorasSyntax = '';
|
||||
|
||||
} else if (lorasListElement) {
|
||||
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
||||
@@ -301,11 +302,42 @@ class RecipeModal {
|
||||
|
||||
if (copyRecipeSyntaxBtn) {
|
||||
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
|
||||
copyToClipboard(text, successMessage) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
|
||||
@@ -399,7 +399,7 @@ export class ImportManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update LoRA count information
|
||||
const totalLoras = this.recipeData.loras.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-content">
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Insert before the buttons container
|
||||
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||
}
|
||||
|
||||
// Update next button text to be more clear
|
||||
nextButton.textContent = 'Continue Without Deleted LoRAs';
|
||||
// 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 {
|
||||
// Remove warning if no deleted LoRAs
|
||||
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';
|
||||
}
|
||||
nextButton.textContent = 'Save Recipe';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user