mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
Add Civitai model retrieval and missing LoRAs download functionality
- Introduced new API endpoints for fetching Civitai model details by model version ID or hash. - Enhanced the download manager to support downloading LoRAs using model version ID or hash, improving flexibility. - Updated RecipeModal to handle missing LoRAs, allowing users to download them directly from the recipe interface. - Added tooltip and click functionality for missing LoRAs status, enhancing user experience. - Improved error handling for missing LoRAs download process, providing clearer feedback to users.
This commit is contained in:
@@ -42,6 +42,8 @@ class ApiRoutes:
|
|||||||
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
||||||
app.router.add_get('/api/folders', routes.get_folders)
|
app.router.add_get('/api/folders', routes.get_folders)
|
||||||
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
||||||
|
app.router.add_get('/api/civitai/model/{modelVersionId}', routes.get_civitai_model)
|
||||||
|
app.router.add_get('/api/civitai/model/{hash}', routes.get_civitai_model)
|
||||||
app.router.add_post('/api/download-lora', routes.download_lora)
|
app.router.add_post('/api/download-lora', routes.download_lora)
|
||||||
app.router.add_post('/api/settings', routes.update_settings)
|
app.router.add_post('/api/settings', routes.update_settings)
|
||||||
app.router.add_post('/api/move_model', routes.move_model)
|
app.router.add_post('/api/move_model', routes.move_model)
|
||||||
@@ -566,6 +568,23 @@ class ApiRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching model versions: {e}")
|
logger.error(f"Error fetching model versions: {e}")
|
||||||
return web.Response(status=500, text=str(e))
|
return web.Response(status=500, text=str(e))
|
||||||
|
|
||||||
|
async def get_civitai_model(self, request: web.Request) -> web.Response:
|
||||||
|
"""Get CivitAI model details by model version ID or hash"""
|
||||||
|
try:
|
||||||
|
model_version_id = request.match_info['modelVersionId']
|
||||||
|
if not model_version_id:
|
||||||
|
hash = request.match_info['hash']
|
||||||
|
model = await self.civitai_client.get_model_by_hash(hash)
|
||||||
|
return web.json_response(model)
|
||||||
|
|
||||||
|
# Get model details from Civitai API
|
||||||
|
model = await self.civitai_client.get_model_version_info(model_version_id)
|
||||||
|
return web.json_response(model)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching model details: {e}")
|
||||||
|
return web.Response(status=500, text=str(e))
|
||||||
|
|
||||||
|
|
||||||
async def download_lora(self, request: web.Request) -> web.Response:
|
async def download_lora(self, request: web.Request) -> web.Response:
|
||||||
async with self._download_lock:
|
async with self._download_lock:
|
||||||
@@ -579,8 +598,22 @@ class ApiRoutes:
|
|||||||
'progress': progress
|
'progress': progress
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Check which identifier is provided
|
||||||
|
download_url = data.get('download_url')
|
||||||
|
model_hash = data.get('model_hash')
|
||||||
|
model_version_id = data.get('model_version_id')
|
||||||
|
|
||||||
|
# Validate that at least one identifier is provided
|
||||||
|
if not any([download_url, model_hash, model_version_id]):
|
||||||
|
return web.Response(
|
||||||
|
status=400,
|
||||||
|
text="Missing required parameter: Please provide either 'download_url', 'hash', or 'modelVersionId'"
|
||||||
|
)
|
||||||
|
|
||||||
result = await self.download_manager.download_from_civitai(
|
result = await self.download_manager.download_from_civitai(
|
||||||
download_url=data.get('download_url'),
|
download_url=download_url,
|
||||||
|
model_hash=model_hash,
|
||||||
|
model_version_id=model_version_id,
|
||||||
save_dir=data.get('lora_root'),
|
save_dir=data.get('lora_root'),
|
||||||
relative_path=data.get('relative_path'),
|
relative_path=data.get('relative_path'),
|
||||||
progress_callback=progress_callback
|
progress_callback=progress_callback
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ class DownloadManager:
|
|||||||
self.civitai_client = CivitaiClient()
|
self.civitai_client = CivitaiClient()
|
||||||
self.file_monitor = file_monitor
|
self.file_monitor = file_monitor
|
||||||
|
|
||||||
async def download_from_civitai(self, download_url: str, save_dir: str, relative_path: str = '',
|
async def download_from_civitai(self, download_url: str = None, model_hash: str = None,
|
||||||
progress_callback=None) -> Dict:
|
model_version_id: str = None, save_dir: str = None,
|
||||||
|
relative_path: str = '', progress_callback=None) -> Dict:
|
||||||
try:
|
try:
|
||||||
# Update save directory with relative path if provided
|
# Update save directory with relative path if provided
|
||||||
if relative_path:
|
if relative_path:
|
||||||
@@ -22,9 +23,21 @@ class DownloadManager:
|
|||||||
# Create directory if it doesn't exist
|
# Create directory if it doesn't exist
|
||||||
os.makedirs(save_dir, exist_ok=True)
|
os.makedirs(save_dir, exist_ok=True)
|
||||||
|
|
||||||
# Get version info
|
# Get version info based on the provided identifier
|
||||||
version_id = download_url.split('/')[-1]
|
version_info = None
|
||||||
version_info = await self.civitai_client.get_model_version_info(version_id)
|
|
||||||
|
if download_url:
|
||||||
|
# Extract version ID from download URL
|
||||||
|
version_id = download_url.split('/')[-1]
|
||||||
|
version_info = await self.civitai_client.get_model_version_info(version_id)
|
||||||
|
elif model_version_id:
|
||||||
|
# Use model version ID directly
|
||||||
|
version_info = await self.civitai_client.get_model_version_info(model_version_id)
|
||||||
|
elif model_hash:
|
||||||
|
# Get model by hash
|
||||||
|
version_info = await self.civitai_client.get_model_by_hash(model_hash)
|
||||||
|
|
||||||
|
|
||||||
if not version_info:
|
if not version_info:
|
||||||
return {'success': False, 'error': 'Failed to fetch model metadata'}
|
return {'success': False, 'error': 'Failed to fetch model metadata'}
|
||||||
|
|
||||||
@@ -89,7 +102,7 @@ class DownloadManager:
|
|||||||
|
|
||||||
# 6. 开始下载流程
|
# 6. 开始下载流程
|
||||||
result = await self._execute_download(
|
result = await self._execute_download(
|
||||||
download_url=download_url,
|
download_url=file_info.get('downloadUrl', ''),
|
||||||
save_dir=save_dir,
|
save_dir=save_dir,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
version_info=version_info,
|
version_info=version_info,
|
||||||
|
|||||||
@@ -655,3 +655,46 @@
|
|||||||
position: fixed; /* Keep as fixed for Chrome */
|
position: fixed; /* Keep as fixed for Chrome */
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add styles for missing LoRAs download feature */
|
||||||
|
.recipe-status.missing {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-status.missing:hover {
|
||||||
|
background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-status.missing .missing-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: var(--z-overlay);
|
||||||
|
width: max-content;
|
||||||
|
max-width: 200px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: -100px;
|
||||||
|
margin-top: -65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-status.missing:hover .missing-tooltip {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-status.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-status.clickable:hover {
|
||||||
|
background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Recipe Modal Component
|
// Recipe Modal Component
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
|
|
||||||
class RecipeModal {
|
class RecipeModal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -50,6 +51,21 @@ class RecipeModal {
|
|||||||
tooltip.style.left = (badgeRect.right - tooltip.offsetWidth) + 'px';
|
tooltip.style.left = (badgeRect.right - tooltip.offsetWidth) + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add tooltip positioning for missing badge
|
||||||
|
if (event.target.closest('.recipe-status.missing')) {
|
||||||
|
const badge = event.target.closest('.recipe-status.missing');
|
||||||
|
const tooltip = badge.querySelector('.missing-tooltip');
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
// Get badge position
|
||||||
|
const badgeRect = badge.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Position the tooltip
|
||||||
|
tooltip.style.top = (badgeRect.bottom + 4) + 'px';
|
||||||
|
tooltip.style.left = (badgeRect.left) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +320,10 @@ class RecipeModal {
|
|||||||
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
||||||
} else if (missingLorasCount > 0) {
|
} else if (missingLorasCount > 0) {
|
||||||
// Some LoRAs are missing (prioritize showing missing over deleted)
|
// Some LoRAs are missing (prioritize showing missing over deleted)
|
||||||
statusHTML = `<div class="recipe-status missing"><i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing</div>`;
|
statusHTML = `<div class="recipe-status missing">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing
|
||||||
|
<div class="missing-tooltip">Click to download missing LoRAs</div>
|
||||||
|
</div>`;
|
||||||
} else if (deletedLorasCount > 0 && missingLorasCount === 0) {
|
} else if (deletedLorasCount > 0 && missingLorasCount === 0) {
|
||||||
// Some LoRAs are deleted but none are missing
|
// Some LoRAs are deleted but none are missing
|
||||||
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle"></i> ${deletedLorasCount} deleted</div>`;
|
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle"></i> ${deletedLorasCount} deleted</div>`;
|
||||||
@@ -312,6 +331,15 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
||||||
|
|
||||||
|
// Add click handler for missing LoRAs status
|
||||||
|
setTimeout(() => {
|
||||||
|
const missingStatus = document.querySelector('.recipe-status.missing');
|
||||||
|
if (missingStatus && missingLorasCount > 0) {
|
||||||
|
missingStatus.classList.add('clickable');
|
||||||
|
missingStatus.addEventListener('click', () => this.showDownloadMissingLorasModal());
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lorasListElement && recipe.loras && recipe.loras.length > 0) {
|
if (lorasListElement && recipe.loras && recipe.loras.length > 0) {
|
||||||
@@ -385,6 +413,8 @@ class RecipeModal {
|
|||||||
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>';
|
||||||
this.recipeLorasSyntax = '';
|
this.recipeLorasSyntax = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(this.currentRecipe.loras);
|
||||||
|
|
||||||
// Show the modal
|
// Show the modal
|
||||||
modalManager.showModal('recipeModal');
|
modalManager.showModal('recipeModal');
|
||||||
@@ -700,6 +730,105 @@ class RecipeModal {
|
|||||||
showToast('Failed to copy text', 'error');
|
showToast('Failed to copy text', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add new method to handle downloading missing LoRAs
|
||||||
|
async showDownloadMissingLorasModal() {
|
||||||
|
console.log("currentRecipe", this.currentRecipe);
|
||||||
|
// Get missing LoRAs from the current recipe
|
||||||
|
const missingLoras = this.currentRecipe.loras.filter(lora => !lora.inLibrary);
|
||||||
|
console.log("missingLoras", missingLoras);
|
||||||
|
|
||||||
|
if (missingLoras.length === 0) {
|
||||||
|
showToast('No missing LoRAs to download', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.loadingManager.showSimpleLoading('Getting version info for missing LoRAs...');
|
||||||
|
|
||||||
|
// Get version info for each missing LoRA by calling the appropriate API endpoint
|
||||||
|
const missingLorasWithVersionInfoPromises = missingLoras.map(async lora => {
|
||||||
|
let endpoint;
|
||||||
|
|
||||||
|
// Determine which endpoint to use based on available data
|
||||||
|
if (lora.modelVersionId) {
|
||||||
|
endpoint = `/api/civitai/model/${lora.modelVersionId}`;
|
||||||
|
} else if (lora.hash) {
|
||||||
|
endpoint = `/api/civitai/model/${lora.hash}`;
|
||||||
|
} else {
|
||||||
|
console.error("Missing both hash and modelVersionId for lora:", lora);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
const versionInfo = await response.json();
|
||||||
|
|
||||||
|
// Return original lora data combined with version info
|
||||||
|
return {
|
||||||
|
...lora,
|
||||||
|
civitaiInfo: versionInfo
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for all API calls to complete
|
||||||
|
const lorasWithVersionInfo = await Promise.all(missingLorasWithVersionInfoPromises);
|
||||||
|
console.log("Loras with version info:", lorasWithVersionInfo);
|
||||||
|
|
||||||
|
// Filter out null values (failed requests)
|
||||||
|
const validLoras = lorasWithVersionInfo.filter(lora => lora !== null);
|
||||||
|
|
||||||
|
if (validLoras.length === 0) {
|
||||||
|
showToast('Failed to get information for missing LoRAs', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the recipe modal first
|
||||||
|
modalManager.closeModal('recipeModal');
|
||||||
|
|
||||||
|
// Prepare data for import manager using the retrieved information
|
||||||
|
const recipeData = {
|
||||||
|
loras: validLoras.map(lora => {
|
||||||
|
const civitaiInfo = lora.civitaiInfo;
|
||||||
|
const modelFile = civitaiInfo.files ?
|
||||||
|
civitaiInfo.files.find(file => file.type === 'Model') : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Basic lora info
|
||||||
|
name: civitaiInfo.model?.name || lora.name,
|
||||||
|
version: civitaiInfo.name || '',
|
||||||
|
strength: lora.strength || 1.0,
|
||||||
|
|
||||||
|
// Model identifiers
|
||||||
|
hash: modelFile?.hashes?.SHA256?.toLowerCase() || lora.hash,
|
||||||
|
modelVersionId: civitaiInfo.id || lora.modelVersionId,
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
thumbnailUrl: civitaiInfo.images?.[0]?.url || '',
|
||||||
|
baseModel: civitaiInfo.baseModel || '',
|
||||||
|
downloadUrl: civitaiInfo.downloadUrl || '',
|
||||||
|
size: modelFile ? (modelFile.sizeKB * 1024) : 0,
|
||||||
|
file_name: modelFile ? modelFile.name.split('.')[0] : '',
|
||||||
|
|
||||||
|
// Status flags
|
||||||
|
existsLocally: false,
|
||||||
|
isDeleted: civitaiInfo.error === "Model not found",
|
||||||
|
isEarlyAccess: !!civitaiInfo.earlyAccessEndsAt,
|
||||||
|
earlyAccessEndsAt: civitaiInfo.earlyAccessEndsAt || ''
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("recipeData for import:", recipeData);
|
||||||
|
|
||||||
|
// Call ImportManager's download missing LoRAs method
|
||||||
|
window.importManager.downloadMissingLoras(recipeData, this.currentRecipe.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error downloading missing LoRAs:", error);
|
||||||
|
showToast('Error preparing LoRAs for download', 'error');
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RecipeModal };
|
export { RecipeModal };
|
||||||
@@ -26,7 +26,7 @@ export class ImportManager {
|
|||||||
this.importMode = 'upload'; // Default mode: 'upload' or 'url'
|
this.importMode = 'upload'; // Default mode: 'upload' or 'url'
|
||||||
}
|
}
|
||||||
|
|
||||||
showImportModal() {
|
showImportModal(recipeData = null, recipeId = null) {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
// Check if modal exists
|
// Check if modal exists
|
||||||
const modal = document.getElementById('importModal');
|
const modal = document.getElementById('importModal');
|
||||||
@@ -39,6 +39,10 @@ export class ImportManager {
|
|||||||
|
|
||||||
// Always reset the state when opening the modal
|
// Always reset the state when opening the modal
|
||||||
this.resetSteps();
|
this.resetSteps();
|
||||||
|
if (recipeData) {
|
||||||
|
this.downloadableLoRAs = recipeData.loras;
|
||||||
|
this.recipeId = recipeId;
|
||||||
|
}
|
||||||
|
|
||||||
// Show the modal
|
// Show the modal
|
||||||
modalManager.showModal('importModal', null, () => {
|
modalManager.showModal('importModal', null, () => {
|
||||||
@@ -831,219 +835,233 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveRecipe() {
|
async saveRecipe() {
|
||||||
if (!this.recipeName) {
|
// Check if we're in download-only mode (for existing recipe)
|
||||||
|
const isDownloadOnly = !!this.recipeId;
|
||||||
|
|
||||||
|
console.log("isDownloadOnly", isDownloadOnly);
|
||||||
|
|
||||||
|
if (!isDownloadOnly && !this.recipeName) {
|
||||||
showToast('Please enter a recipe name', 'error');
|
showToast('Please enter a recipe name', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First save the recipe
|
this.loadingManager.showSimpleLoading(isDownloadOnly ? 'Preparing download...' : 'Saving recipe...');
|
||||||
this.loadingManager.showSimpleLoading('Saving recipe...');
|
|
||||||
|
|
||||||
// Create form data for save request
|
// If we're only downloading LoRAs for an existing recipe, skip the recipe save step
|
||||||
const formData = new FormData();
|
if (!isDownloadOnly) {
|
||||||
|
// First save the recipe
|
||||||
// Handle image data - either from file upload or from URL mode
|
// Create form data for save request
|
||||||
if (this.recipeImage) {
|
const formData = new FormData();
|
||||||
// File upload mode
|
|
||||||
formData.append('image', this.recipeImage);
|
// Handle image data - either from file upload or from URL mode
|
||||||
} else if (this.recipeData && this.recipeData.image_base64) {
|
if (this.recipeImage) {
|
||||||
// URL mode with base64 data
|
// File upload mode
|
||||||
formData.append('image_base64', this.recipeData.image_base64);
|
formData.append('image', this.recipeImage);
|
||||||
} else if (this.importMode === 'url') {
|
} else if (this.recipeData && this.recipeData.image_base64) {
|
||||||
// Fallback for URL mode - tell backend to fetch the image again
|
// URL mode with base64 data
|
||||||
const urlInput = document.getElementById('imageUrlInput');
|
formData.append('image_base64', this.recipeData.image_base64);
|
||||||
if (urlInput && urlInput.value) {
|
} else if (this.importMode === 'url') {
|
||||||
formData.append('image_url', urlInput.value);
|
// 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 {
|
} else {
|
||||||
throw new Error('No image data available');
|
throw new Error('No image data available');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error('No image data available');
|
formData.append('name', this.recipeName);
|
||||||
|
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||||
|
|
||||||
|
// Prepare complete metadata including generation parameters
|
||||||
|
const completeMetadata = {
|
||||||
|
base_model: this.recipeData.base_model || "",
|
||||||
|
loras: this.recipeData.loras || [],
|
||||||
|
gen_params: this.recipeData.gen_params || {},
|
||||||
|
raw_metadata: this.recipeData.raw_metadata || {}
|
||||||
|
};
|
||||||
|
|
||||||
|
formData.append('metadata', JSON.stringify(completeMetadata));
|
||||||
|
|
||||||
|
// Send save request
|
||||||
|
const response = await fetch('/api/recipes/save', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
// Handle save error
|
||||||
|
console.error("Failed to save recipe:", result.error);
|
||||||
|
showToast(result.error, 'error');
|
||||||
|
// Close modal
|
||||||
|
modalManager.closeModal('importModal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append('name', this.recipeName);
|
// Check if we need to download LoRAs
|
||||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
if (this.downloadableLoRAs && this.downloadableLoRAs.length > 0) {
|
||||||
|
// For download, we need to validate the target path
|
||||||
// Prepare complete metadata including generation parameters
|
const loraRoot = document.getElementById('importLoraRoot')?.value;
|
||||||
const completeMetadata = {
|
if (!loraRoot) {
|
||||||
base_model: this.recipeData.base_model || "",
|
throw new Error('Please select a LoRA root directory');
|
||||||
loras: this.recipeData.loras || [],
|
}
|
||||||
gen_params: this.recipeData.gen_params || {},
|
|
||||||
raw_metadata: this.recipeData.raw_metadata || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
formData.append('metadata', JSON.stringify(completeMetadata));
|
|
||||||
|
|
||||||
// Send save request
|
|
||||||
const response = await fetch('/api/recipes/save', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
// Handle successful save
|
|
||||||
|
|
||||||
|
// Build target path
|
||||||
|
let targetPath = loraRoot;
|
||||||
|
if (this.selectedFolder) {
|
||||||
|
targetPath += '/' + this.selectedFolder;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we need to download LoRAs
|
const newFolder = document.getElementById('importNewFolder')?.value?.trim();
|
||||||
if (this.downloadableLoRAs && this.downloadableLoRAs.length > 0) {
|
if (newFolder) {
|
||||||
// For download, we need to validate the target path
|
targetPath += '/' + newFolder;
|
||||||
const loraRoot = document.getElementById('importLoraRoot')?.value;
|
}
|
||||||
if (!loraRoot) {
|
|
||||||
throw new Error('Please select a LoRA root directory');
|
// Set up WebSocket for progress updates
|
||||||
}
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||||
// Build target path
|
|
||||||
let targetPath = loraRoot;
|
// Show enhanced loading with progress details for multiple items
|
||||||
if (this.selectedFolder) {
|
const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length);
|
||||||
targetPath += '/' + this.selectedFolder;
|
|
||||||
}
|
let completedDownloads = 0;
|
||||||
|
let failedDownloads = 0;
|
||||||
const newFolder = document.getElementById('importNewFolder')?.value?.trim();
|
let earlyAccessFailures = 0;
|
||||||
if (newFolder) {
|
let currentLoraProgress = 0;
|
||||||
targetPath += '/' + newFolder;
|
|
||||||
}
|
// Set up progress tracking for current download
|
||||||
|
ws.onmessage = (event) => {
|
||||||
// Set up WebSocket for progress updates
|
const data = JSON.parse(event.data);
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
if (data.status === 'progress') {
|
||||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
// Update current LoRA progress
|
||||||
|
currentLoraProgress = data.progress;
|
||||||
// Show enhanced loading with progress details for multiple items
|
|
||||||
const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length);
|
|
||||||
|
|
||||||
let completedDownloads = 0;
|
|
||||||
let failedDownloads = 0;
|
|
||||||
let earlyAccessFailures = 0;
|
|
||||||
let currentLoraProgress = 0;
|
|
||||||
|
|
||||||
// Set up progress tracking for current download
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if (data.status === 'progress') {
|
|
||||||
// Update current LoRA progress
|
|
||||||
currentLoraProgress = data.progress;
|
|
||||||
|
|
||||||
// Get current LoRA name
|
|
||||||
const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads];
|
|
||||||
const loraName = currentLora ? currentLora.name : '';
|
|
||||||
|
|
||||||
// Update progress display
|
|
||||||
updateProgress(currentLoraProgress, completedDownloads, loraName);
|
|
||||||
|
|
||||||
// Add more detailed status messages based on progress
|
|
||||||
if (currentLoraProgress < 3) {
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
|
||||||
);
|
|
||||||
} else if (currentLoraProgress === 3) {
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
|
||||||
);
|
|
||||||
} else if (currentLoraProgress > 3 && currentLoraProgress < 100) {
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < this.downloadableLoRAs.length; i++) {
|
|
||||||
const lora = this.downloadableLoRAs[i];
|
|
||||||
|
|
||||||
// Reset current LoRA progress for new download
|
// Get current LoRA name
|
||||||
currentLoraProgress = 0;
|
const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads];
|
||||||
|
const loraName = currentLora ? currentLora.name : '';
|
||||||
|
|
||||||
// Initial status update for new LoRA
|
// Update progress display
|
||||||
this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.downloadableLoRAs.length}`);
|
updateProgress(currentLoraProgress, completedDownloads, loraName);
|
||||||
updateProgress(0, completedDownloads, lora.name);
|
|
||||||
|
|
||||||
try {
|
// Add more detailed status messages based on progress
|
||||||
// Download the LoRA
|
if (currentLoraProgress < 3) {
|
||||||
const response = await fetch('/api/download-lora', {
|
this.loadingManager.setStatus(
|
||||||
method: 'POST',
|
`Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
headers: { 'Content-Type': 'application/json' },
|
);
|
||||||
body: JSON.stringify({
|
} else if (currentLoraProgress === 3) {
|
||||||
download_url: lora.downloadUrl,
|
this.loadingManager.setStatus(
|
||||||
lora_root: loraRoot,
|
`Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
relative_path: targetPath.replace(loraRoot + '/', '')
|
);
|
||||||
})
|
} else if (currentLoraProgress > 3 && currentLoraProgress < 100) {
|
||||||
});
|
this.loadingManager.setStatus(
|
||||||
|
`Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
|
||||||
|
|
||||||
// Check if this is an early access error (status 401 is the key indicator)
|
|
||||||
if (response.status === 401 ||
|
|
||||||
(errorText.toLowerCase().includes('early access') ||
|
|
||||||
errorText.toLowerCase().includes('purchase'))) {
|
|
||||||
earlyAccessFailures++;
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Failed to download ${lora.name}: Early Access required`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
failedDownloads++;
|
|
||||||
// Continue with next download
|
|
||||||
} else {
|
|
||||||
completedDownloads++;
|
|
||||||
|
|
||||||
// Update progress to show completion of current LoRA
|
|
||||||
updateProgress(100, completedDownloads, '');
|
|
||||||
|
|
||||||
if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) {
|
|
||||||
this.loadingManager.setStatus(
|
|
||||||
`Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (downloadError) {
|
|
||||||
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
|
||||||
failedDownloads++;
|
|
||||||
// Continue with next download
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close WebSocket
|
|
||||||
ws.close();
|
|
||||||
|
|
||||||
// Show appropriate completion message based on results
|
|
||||||
if (failedDownloads === 0) {
|
|
||||||
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
|
||||||
} else {
|
|
||||||
if (earlyAccessFailures > 0) {
|
|
||||||
showToast(
|
|
||||||
`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`,
|
|
||||||
'error'
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error');
|
this.loadingManager.setStatus(
|
||||||
|
`Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < this.downloadableLoRAs.length; i++) {
|
||||||
|
const lora = this.downloadableLoRAs[i];
|
||||||
|
|
||||||
|
// Reset current LoRA progress for new download
|
||||||
|
currentLoraProgress = 0;
|
||||||
|
|
||||||
|
// Initial status update for new LoRA
|
||||||
|
this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.downloadableLoRAs.length}`);
|
||||||
|
updateProgress(0, completedDownloads, lora.name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download the LoRA
|
||||||
|
const response = await fetch('/api/download-lora', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
download_url: lora.downloadUrl,
|
||||||
|
model_version_id: lora.modelVersionId,
|
||||||
|
model_hash: lora.hash,
|
||||||
|
lora_root: loraRoot,
|
||||||
|
relative_path: targetPath.replace(loraRoot + '/', '')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
||||||
|
|
||||||
|
// Check if this is an early access error (status 401 is the key indicator)
|
||||||
|
if (response.status === 401 ||
|
||||||
|
(errorText.toLowerCase().includes('early access') ||
|
||||||
|
errorText.toLowerCase().includes('purchase'))) {
|
||||||
|
earlyAccessFailures++;
|
||||||
|
this.loadingManager.setStatus(
|
||||||
|
`Failed to download ${lora.name}: Early Access required`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
failedDownloads++;
|
||||||
|
// Continue with next download
|
||||||
|
} else {
|
||||||
|
completedDownloads++;
|
||||||
|
|
||||||
|
// Update progress to show completion of current LoRA
|
||||||
|
updateProgress(100, completedDownloads, '');
|
||||||
|
|
||||||
|
if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) {
|
||||||
|
this.loadingManager.setStatus(
|
||||||
|
`Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (downloadError) {
|
||||||
|
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
||||||
|
failedDownloads++;
|
||||||
|
// Continue with next download
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close WebSocket
|
||||||
|
ws.close();
|
||||||
|
|
||||||
|
// Show appropriate completion message based on results
|
||||||
|
if (failedDownloads === 0) {
|
||||||
|
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
||||||
|
} else {
|
||||||
|
if (earlyAccessFailures > 0) {
|
||||||
|
showToast(
|
||||||
|
`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show success message for recipe save
|
// Show success message
|
||||||
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
if (isDownloadOnly) {
|
||||||
|
showToast('LoRAs downloaded successfully', 'success');
|
||||||
// Close modal and reload recipes
|
|
||||||
modalManager.closeModal('importModal');
|
|
||||||
|
|
||||||
window.recipeManager.loadRecipes(true); // true to reset pagination
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Handle error
|
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
||||||
console.error(`Failed to save recipe: ${result.error}`);
|
|
||||||
// Show error message to user
|
|
||||||
showToast(result.error, 'error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
modalManager.closeModal('importModal');
|
||||||
|
|
||||||
|
// Refresh the recipe
|
||||||
|
window.recipeManager.loadRecipes(this.recipeId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving recipe:', error);
|
console.error('Error:', error);
|
||||||
showToast(error.message, 'error');
|
showToast(error.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingManager.hide();
|
this.loadingManager.hide();
|
||||||
@@ -1205,4 +1223,33 @@ export class ImportManager {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add new method to handle downloading missing LoRAs from a recipe
|
||||||
|
downloadMissingLoras(recipeData, recipeId) {
|
||||||
|
// Store the recipe data and ID
|
||||||
|
this.recipeData = recipeData;
|
||||||
|
this.recipeId = recipeId;
|
||||||
|
|
||||||
|
// Show the location step directly
|
||||||
|
this.showImportModal(recipeData, recipeId);
|
||||||
|
this.proceedToLocation();
|
||||||
|
|
||||||
|
// Update the modal title to reflect we're downloading for an existing recipe
|
||||||
|
const modalTitle = document.querySelector('#importModal h2');
|
||||||
|
if (modalTitle) {
|
||||||
|
modalTitle.textContent = 'Download Missing LoRAs';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the save button text
|
||||||
|
const saveButton = document.querySelector('#locationStep .primary-btn');
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.textContent = 'Download Missing LoRAs';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the back button since we're skipping steps
|
||||||
|
const backButton = document.querySelector('#locationStep .secondary-btn');
|
||||||
|
if (backButton) {
|
||||||
|
backButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user