mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Implement model description retrieval and update related API endpoints
This commit is contained in:
@@ -70,6 +70,7 @@ class BaseModelRoutes(ABC):
|
|||||||
app.router.add_get(f'/api/{prefix}/preview-url', self.get_model_preview_url)
|
app.router.add_get(f'/api/{prefix}/preview-url', self.get_model_preview_url)
|
||||||
app.router.add_get(f'/api/{prefix}/civitai-url', self.get_model_civitai_url)
|
app.router.add_get(f'/api/{prefix}/civitai-url', self.get_model_civitai_url)
|
||||||
app.router.add_get(f'/api/{prefix}/metadata', self.get_model_metadata)
|
app.router.add_get(f'/api/{prefix}/metadata', self.get_model_metadata)
|
||||||
|
app.router.add_get(f'/api/{prefix}/model-description', self.get_model_description)
|
||||||
|
|
||||||
# Autocomplete route
|
# Autocomplete route
|
||||||
app.router.add_get(f'/api/{prefix}/relative-paths', self.get_relative_paths)
|
app.router.add_get(f'/api/{prefix}/relative-paths', self.get_relative_paths)
|
||||||
@@ -1165,6 +1166,32 @@ class BaseModelRoutes(ABC):
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
|
async def get_model_description(self, request: web.Request) -> web.Response:
|
||||||
|
"""Get model description by file path"""
|
||||||
|
try:
|
||||||
|
file_path = request.query.get('file_path')
|
||||||
|
if not file_path:
|
||||||
|
return web.Response(text='File path is required', status=400)
|
||||||
|
|
||||||
|
description = await self.service.get_model_description(file_path)
|
||||||
|
if description is not None:
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'description': description
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': f'{self.model_type.capitalize()} not found or no description available'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting {self.model_type} description: {e}", exc_info=True)
|
||||||
|
return web.json_response({
|
||||||
|
'success': False,
|
||||||
|
'error': str(e)
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
async def get_relative_paths(self, request: web.Request) -> web.Response:
|
async def get_relative_paths(self, request: web.Request) -> web.Response:
|
||||||
"""Get model relative file paths for autocomplete functionality"""
|
"""Get model relative file paths for autocomplete functionality"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ class LoraRoutes(BaseModelRoutes):
|
|||||||
# LoRA-specific query routes
|
# LoRA-specific query routes
|
||||||
app.router.add_get(f'/api/{prefix}/letter-counts', self.get_letter_counts)
|
app.router.add_get(f'/api/{prefix}/letter-counts', self.get_letter_counts)
|
||||||
app.router.add_get(f'/api/{prefix}/get-trigger-words', self.get_lora_trigger_words)
|
app.router.add_get(f'/api/{prefix}/get-trigger-words', self.get_lora_trigger_words)
|
||||||
app.router.add_get(f'/api/{prefix}/model-description', self.get_lora_model_description)
|
|
||||||
app.router.add_get(f'/api/{prefix}/usage-tips-by-path', self.get_lora_usage_tips_by_path)
|
app.router.add_get(f'/api/{prefix}/usage-tips-by-path', self.get_lora_usage_tips_by_path)
|
||||||
|
|
||||||
# CivitAI integration with LoRA-specific validation
|
# CivitAI integration with LoRA-specific validation
|
||||||
@@ -298,74 +297,6 @@ class LoraRoutes(BaseModelRoutes):
|
|||||||
"error": str(e)
|
"error": str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
async def get_lora_model_description(self, request: web.Request) -> web.Response:
|
|
||||||
"""Get model description for a Lora model"""
|
|
||||||
try:
|
|
||||||
# Get parameters
|
|
||||||
model_id = request.query.get('model_id')
|
|
||||||
file_path = request.query.get('file_path')
|
|
||||||
|
|
||||||
if not model_id:
|
|
||||||
return web.json_response({
|
|
||||||
'success': False,
|
|
||||||
'error': 'Model ID is required'
|
|
||||||
}, status=400)
|
|
||||||
|
|
||||||
# Check if we already have the description stored in metadata
|
|
||||||
description = None
|
|
||||||
tags = []
|
|
||||||
creator = {}
|
|
||||||
if file_path:
|
|
||||||
import os
|
|
||||||
from ..utils.metadata_manager import MetadataManager
|
|
||||||
metadata_path = os.path.splitext(file_path)[0] + '.metadata.json'
|
|
||||||
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
|
||||||
description = metadata.get('modelDescription')
|
|
||||||
tags = metadata.get('tags', [])
|
|
||||||
creator = metadata.get('creator', {})
|
|
||||||
|
|
||||||
# If description is not in metadata, fetch from CivitAI
|
|
||||||
if not description:
|
|
||||||
logger.info(f"Fetching model metadata for model ID: {model_id}")
|
|
||||||
model_metadata, _ = await self.civitai_client.get_model_metadata(model_id)
|
|
||||||
|
|
||||||
if model_metadata:
|
|
||||||
description = model_metadata.get('description')
|
|
||||||
tags = model_metadata.get('tags', [])
|
|
||||||
creator = model_metadata.get('creator', {})
|
|
||||||
|
|
||||||
# Save the metadata to file if we have a file path and got metadata
|
|
||||||
if file_path:
|
|
||||||
try:
|
|
||||||
metadata_path = os.path.splitext(file_path)[0] + '.metadata.json'
|
|
||||||
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
|
||||||
|
|
||||||
metadata['modelDescription'] = description
|
|
||||||
metadata['tags'] = tags
|
|
||||||
# Ensure the civitai dict exists
|
|
||||||
if 'civitai' not in metadata:
|
|
||||||
metadata['civitai'] = {}
|
|
||||||
# Store creator in the civitai nested structure
|
|
||||||
metadata['civitai']['creator'] = creator
|
|
||||||
|
|
||||||
await MetadataManager.save_metadata(file_path, metadata)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error saving model metadata: {e}")
|
|
||||||
|
|
||||||
return web.json_response({
|
|
||||||
'success': True,
|
|
||||||
'description': description or "<p>No model description available.</p>",
|
|
||||||
'tags': tags,
|
|
||||||
'creator': creator
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting model metadata: {e}")
|
|
||||||
return web.json_response({
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}, status=500)
|
|
||||||
|
|
||||||
async def get_trigger_words(self, request: web.Request) -> web.Response:
|
async def get_trigger_words(self, request: web.Request) -> web.Response:
|
||||||
"""Get trigger words for specified LoRA models"""
|
"""Get trigger words for specified LoRA models"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -390,6 +390,16 @@ class BaseModelService(ABC):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def get_model_description(self, file_path: str) -> Optional[str]:
|
||||||
|
"""Get model description by file path"""
|
||||||
|
cache = await self.scanner.get_cached_data()
|
||||||
|
|
||||||
|
for model in cache.raw_data:
|
||||||
|
if model.get('file_path') == file_path:
|
||||||
|
return model.get('modelDescription', '')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def search_relative_paths(self, search_term: str, limit: int = 15) -> List[str]:
|
async def search_relative_paths(self, search_term: str, limit: int = 15) -> List[str]:
|
||||||
"""Search model relative file paths for autocomplete functionality"""
|
"""Search model relative file paths for autocomplete functionality"""
|
||||||
cache = await self.scanner.get_cached_data()
|
cache = await self.scanner.get_cached_data()
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function getApiEndpoints(modelType) {
|
|||||||
conflicts: `/api/${modelType}/find-filename-conflicts`,
|
conflicts: `/api/${modelType}/find-filename-conflicts`,
|
||||||
verify: `/api/${modelType}/verify-duplicates`,
|
verify: `/api/${modelType}/verify-duplicates`,
|
||||||
metadata: `/api/${modelType}/metadata`,
|
metadata: `/api/${modelType}/metadata`,
|
||||||
|
modelDescription: `/api/${modelType}/model-description`,
|
||||||
|
|
||||||
// Model-specific endpoints (will be merged with specific configs)
|
// Model-specific endpoints (will be merged with specific configs)
|
||||||
specific: {}
|
specific: {}
|
||||||
@@ -106,7 +107,6 @@ export const MODEL_SPECIFIC_ENDPOINTS = {
|
|||||||
previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`,
|
previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`,
|
||||||
civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`,
|
civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`,
|
||||||
metadata: `/api/${MODEL_TYPES.LORA}/metadata`,
|
metadata: `/api/${MODEL_TYPES.LORA}/metadata`,
|
||||||
modelDescription: `/api/${MODEL_TYPES.LORA}/model-description`,
|
|
||||||
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
|
getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`,
|
||||||
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
|
civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`,
|
||||||
civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`,
|
civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`,
|
||||||
|
|||||||
@@ -970,4 +970,26 @@ export class BaseModelApiClient {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchModelDescription(filePath) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ file_path: filePath });
|
||||||
|
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
return data.description;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || `No description found for ${this.apiConfig.config.singularName}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching ${this.apiConfig.config.singularName} description:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BaseModelApiClient } from './baseModelApi.js';
|
import { BaseModelApiClient } from './baseModelApi.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
|
||||||
import { getSessionItem } from '../utils/storageHelpers.js';
|
import { getSessionItem } from '../utils/storageHelpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function setupTabSwitching() {
|
|||||||
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
||||||
|
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', async () => {
|
||||||
// Remove active class from all tabs
|
// Remove active class from all tabs
|
||||||
document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn =>
|
document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn =>
|
||||||
btn.classList.remove('active')
|
btn.classList.remove('active')
|
||||||
@@ -26,24 +26,58 @@ export function setupTabSwitching() {
|
|||||||
const tabId = `${button.dataset.tab}-tab`;
|
const tabId = `${button.dataset.tab}-tab`;
|
||||||
document.getElementById(tabId).classList.add('active');
|
document.getElementById(tabId).classList.add('active');
|
||||||
|
|
||||||
// If switching to description tab, make sure content is properly sized
|
// If switching to description tab, load content lazily
|
||||||
if (button.dataset.tab === 'description') {
|
if (button.dataset.tab === 'description') {
|
||||||
const descriptionContent = document.querySelector('.model-description-content');
|
await loadModelDescription();
|
||||||
if (descriptionContent) {
|
|
||||||
const hasContent = descriptionContent.innerHTML.trim() !== '';
|
|
||||||
document.querySelector('.model-description-loading')?.classList.add('hidden');
|
|
||||||
|
|
||||||
// If no content, show a message
|
|
||||||
if (!hasContent) {
|
|
||||||
descriptionContent.innerHTML = '<div class="no-description">No model description available</div>';
|
|
||||||
descriptionContent.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load model description lazily
|
||||||
|
*/
|
||||||
|
async function loadModelDescription() {
|
||||||
|
const descriptionContent = document.querySelector('.model-description-content');
|
||||||
|
const descriptionLoading = document.querySelector('.model-description-loading');
|
||||||
|
const showcaseSection = document.querySelector('.showcase-section');
|
||||||
|
|
||||||
|
if (!descriptionContent || !showcaseSection) return;
|
||||||
|
|
||||||
|
// Check if already loaded
|
||||||
|
if (descriptionContent.dataset.loaded === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = showcaseSection.dataset.filepath;
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
descriptionLoading?.classList.remove('hidden');
|
||||||
|
descriptionContent.classList.add('hidden');
|
||||||
|
|
||||||
|
// Fetch description from API
|
||||||
|
const { getModelApiClient } = await import('../../api/modelApiFactory.js');
|
||||||
|
const description = await getModelApiClient().fetchModelDescription(filePath);
|
||||||
|
|
||||||
|
// Update content
|
||||||
|
descriptionContent.innerHTML = description || '<div class="no-description">No model description available</div>';
|
||||||
|
descriptionContent.dataset.loaded = 'true';
|
||||||
|
|
||||||
|
// Set up editing functionality
|
||||||
|
setupModelDescriptionEditing(filePath);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading model description:', error);
|
||||||
|
descriptionContent.innerHTML = '<div class="no-description">Failed to load model description</div>';
|
||||||
|
} finally {
|
||||||
|
// Hide loading state
|
||||||
|
descriptionLoading?.classList.add('hidden');
|
||||||
|
descriptionContent.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up model description editing functionality
|
* Set up model description editing functionality
|
||||||
* @param {string} filePath - File path
|
* @param {string} filePath - File path
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ export function showModelModal(model, modelType) {
|
|||||||
<div class="model-description-loading">
|
<div class="model-description-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
||||||
</div>
|
</div>
|
||||||
<div class="model-description-content">
|
<div class="model-description-content hidden">
|
||||||
${model.modelDescription || ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,8 +83,7 @@ export function showModelModal(model, modelType) {
|
|||||||
<div class="model-description-loading">
|
<div class="model-description-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
<i class="fas fa-spinner fa-spin"></i> Loading model description...
|
||||||
</div>
|
</div>
|
||||||
<div class="model-description-content">
|
<div class="model-description-content hidden">
|
||||||
${model.modelDescription || ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -210,7 +208,7 @@ export function showModelModal(model, modelType) {
|
|||||||
setupModelNameEditing(model.file_path);
|
setupModelNameEditing(model.file_path);
|
||||||
setupBaseModelEditing(model.file_path);
|
setupBaseModelEditing(model.file_path);
|
||||||
setupFileNameEditing(model.file_path);
|
setupFileNameEditing(model.file_path);
|
||||||
setupModelDescriptionEditing(model.file_path, model.modelDescription || '');
|
// Remove setupModelDescriptionEditing from here - it will be called lazily
|
||||||
setupEventHandlers(model.file_path);
|
setupEventHandlers(model.file_path);
|
||||||
|
|
||||||
// LoRA specific setup
|
// LoRA specific setup
|
||||||
|
|||||||
Reference in New Issue
Block a user