From 5b0becaaf2513d39a11c7b63b8dc0a4a0d780468 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 27 Aug 2025 18:22:56 +0800 Subject: [PATCH] feat: Implement model description retrieval and update related API endpoints --- py/routes/base_model_routes.py | 27 ++++++++ py/routes/lora_routes.py | 69 ------------------- py/services/base_model_service.py | 10 +++ static/js/api/apiConfig.js | 2 +- static/js/api/baseModelApi.js | 22 ++++++ static/js/api/loraApi.js | 1 - .../js/components/shared/ModelDescription.js | 60 ++++++++++++---- static/js/components/shared/ModelModal.js | 8 +-- 8 files changed, 110 insertions(+), 89 deletions(-) diff --git a/py/routes/base_model_routes.py b/py/routes/base_model_routes.py index 5f770d51..ee1f03ca 100644 --- a/py/routes/base_model_routes.py +++ b/py/routes/base_model_routes.py @@ -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}/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}/model-description', self.get_model_description) # Autocomplete route app.router.add_get(f'/api/{prefix}/relative-paths', self.get_relative_paths) @@ -1165,6 +1166,32 @@ class BaseModelRoutes(ABC): 'error': str(e) }, 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: """Get model relative file paths for autocomplete functionality""" try: diff --git a/py/routes/lora_routes.py b/py/routes/lora_routes.py index cb99b66f..2da33cb1 100644 --- a/py/routes/lora_routes.py +++ b/py/routes/lora_routes.py @@ -44,7 +44,6 @@ class LoraRoutes(BaseModelRoutes): # LoRA-specific query routes 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}/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) # CivitAI integration with LoRA-specific validation @@ -298,74 +297,6 @@ class LoraRoutes(BaseModelRoutes): "error": str(e) }, 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 "
No model description available.
", - '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: """Get trigger words for specified LoRA models""" try: diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index bb52df50..bd1b5f1f 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -390,6 +390,16 @@ class BaseModelService(ABC): 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]: """Search model relative file paths for autocomplete functionality""" cache = await self.scanner.get_cached_data() diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js index d2e5ab2b..73638e20 100644 --- a/static/js/api/apiConfig.js +++ b/static/js/api/apiConfig.js @@ -89,6 +89,7 @@ export function getApiEndpoints(modelType) { conflicts: `/api/${modelType}/find-filename-conflicts`, verify: `/api/${modelType}/verify-duplicates`, metadata: `/api/${modelType}/metadata`, + modelDescription: `/api/${modelType}/model-description`, // Model-specific endpoints (will be merged with specific configs) specific: {} @@ -106,7 +107,6 @@ export const MODEL_SPECIFIC_ENDPOINTS = { previewUrl: `/api/${MODEL_TYPES.LORA}/preview-url`, civitaiUrl: `/api/${MODEL_TYPES.LORA}/civitai-url`, metadata: `/api/${MODEL_TYPES.LORA}/metadata`, - modelDescription: `/api/${MODEL_TYPES.LORA}/model-description`, getTriggerWordsPost: `/api/${MODEL_TYPES.LORA}/get_trigger_words`, civitaiModelByVersion: `/api/${MODEL_TYPES.LORA}/civitai/model/version`, civitaiModelByHash: `/api/${MODEL_TYPES.LORA}/civitai/model/hash`, diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 98c82eea..0e96dfa5 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -970,4 +970,26 @@ export class BaseModelApiClient { 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; + } + } } \ No newline at end of file diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 693fe5bc..d93cb1a8 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -1,5 +1,4 @@ import { BaseModelApiClient } from './baseModelApi.js'; -import { showToast } from '../utils/uiHelpers.js'; import { getSessionItem } from '../utils/storageHelpers.js'; /** diff --git a/static/js/components/shared/ModelDescription.js b/static/js/components/shared/ModelDescription.js index fefb97af..4699090e 100644 --- a/static/js/components/shared/ModelDescription.js +++ b/static/js/components/shared/ModelDescription.js @@ -12,7 +12,7 @@ export function setupTabSwitching() { const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn'); tabButtons.forEach(button => { - button.addEventListener('click', () => { + button.addEventListener('click', async () => { // Remove active class from all tabs document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn => btn.classList.remove('active') @@ -26,24 +26,58 @@ export function setupTabSwitching() { const tabId = `${button.dataset.tab}-tab`; 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') { - const descriptionContent = document.querySelector('.model-description-content'); - 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 = '