| """ |
| Edit Models Configuration - Centralized model management for Edit features. |
| |
| This module provides: |
| - Detection of installed ComfyUI models for upscale/enhance/face restoration |
| - Default model configuration with fallbacks |
| - User-configurable model preferences |
| - Consistent model access across enhance.py, upscale.py, etc. |
| |
| Model Categories: |
| - UPSCALE: 4x-UltraSharp, RealESRGAN, realesr-general, SwinIR |
| - FACE_RESTORE: GFPGANv1.4, CodeFormer |
| - BACKGROUND: u2net (rembg), SAM |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| from dataclasses import dataclass, field |
| from enum import Enum |
| from pathlib import Path |
| from typing import Dict, List, Optional, Any |
|
|
| from .providers import get_comfy_models_path |
|
|
|
|
| class ModelCategory(str, Enum): |
| """Categories of edit models.""" |
| UPSCALE = "upscale" |
| FACE_RESTORE = "face_restore" |
| BACKGROUND = "background" |
| |
| AVATAR_GENERATION = "avatar_generation" |
|
|
|
|
| @dataclass |
| class ModelInfo: |
| """Information about a single model.""" |
| id: str |
| name: str |
| category: ModelCategory |
| filename: str |
| subdir: str |
| description: str = "" |
| is_default: bool = False |
| |
| license: str = "" |
| commercial_use_ok: Optional[bool] = None |
| homepage: str = "" |
| download_url: str = "" |
| sha256: str = "" |
| requires: List[str] = field(default_factory=list) |
|
|
| @property |
| def path(self) -> Path: |
| """Get the full path to this model.""" |
| models_path = get_comfy_models_path() |
| return models_path / self.subdir / self.filename |
|
|
| @property |
| def installed(self) -> bool: |
| """Check if this model is installed.""" |
| p = self.path |
| return p.exists() and p.stat().st_size > 0 |
|
|
|
|
| |
| |
| |
|
|
| UPSCALE_MODELS: Dict[str, ModelInfo] = { |
| "4x-UltraSharp": ModelInfo( |
| id="4x-UltraSharp", |
| name="4x UltraSharp", |
| category=ModelCategory.UPSCALE, |
| filename="4x-UltraSharp.pth", |
| subdir="upscale_models", |
| description="High-quality upscaling with sharp details", |
| is_default=True, |
| ), |
| "RealESRGAN_x4plus": ModelInfo( |
| id="RealESRGAN_x4plus", |
| name="RealESRGAN x4+", |
| category=ModelCategory.UPSCALE, |
| filename="RealESRGAN_x4plus.pth", |
| subdir="upscale_models", |
| description="Photo enhancement with natural texture recovery", |
| ), |
| "realesr-general-x4v3": ModelInfo( |
| id="realesr-general-x4v3", |
| name="RealESRGAN General v3", |
| category=ModelCategory.UPSCALE, |
| filename="realesr-general-x4v3.pth", |
| subdir="upscale_models", |
| description="General purpose upscaling", |
| ), |
| "SwinIR_4x": ModelInfo( |
| id="SwinIR_4x", |
| name="SwinIR 4x", |
| category=ModelCategory.UPSCALE, |
| filename="SwinIR_4x.pth", |
| subdir="upscale_models", |
| description="Artifact removal and restoration", |
| ), |
| } |
|
|
| FACE_RESTORE_MODELS: Dict[str, ModelInfo] = { |
| "GFPGANv1.4": ModelInfo( |
| id="GFPGANv1.4", |
| name="GFPGAN v1.4", |
| category=ModelCategory.FACE_RESTORE, |
| filename="GFPGANv1.4.pth", |
| subdir="gfpgan", |
| description="Face restoration and enhancement", |
| is_default=True, |
| ), |
| "codeformer": ModelInfo( |
| id="codeformer", |
| name="CodeFormer", |
| category=ModelCategory.FACE_RESTORE, |
| filename="codeformer.pth", |
| subdir="codeformer", |
| description="AI face restoration with fidelity control", |
| ), |
| } |
|
|
| BACKGROUND_MODELS: Dict[str, ModelInfo] = { |
| "u2net": ModelInfo( |
| id="u2net", |
| name="U2-Net", |
| category=ModelCategory.BACKGROUND, |
| filename="u2net.onnx", |
| subdir="rembg", |
| description="Background removal segmentation", |
| is_default=True, |
| ), |
| "sam_vit_h": ModelInfo( |
| id="sam_vit_h", |
| name="SAM ViT-H", |
| category=ModelCategory.BACKGROUND, |
| filename="sam_vit_h_4b8939.pth", |
| subdir="sams", |
| description="Segment Anything Model (high quality)", |
| ), |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| AVATAR_GENERATION_MODELS: Dict[str, ModelInfo] = { |
| |
| "insightface-antelopev2": ModelInfo( |
| id="insightface-antelopev2", |
| name="InsightFace AntelopeV2", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="antelopev2.zip", |
| subdir="insightface/models", |
| description="Face detection & embedding pack used by InstantID and face-swap workflows.", |
| license="InsightFace model zoo (code MIT, models vary)", |
| commercial_use_ok=True, |
| homepage="https://github.com/deepinsight/insightface", |
| download_url="https://huggingface.co/MonsterMMORPG/tools/resolve/main/antelopev2.zip", |
| sha256="", |
| requires=[], |
| is_default=True, |
| ), |
|
|
| |
| "insightface-inswapper-128": ModelInfo( |
| id="insightface-inswapper-128", |
| name="InsightFace InSwapper 128 (ONNX)", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="inswapper_128.onnx", |
| subdir="insightface", |
| description="Face swap ONNX model for consistent identity transfer onto generated images.", |
| license="Model distribution varies (see source)", |
| commercial_use_ok=True, |
| homepage="https://github.com/deepinsight/insightface", |
| download_url="https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx", |
| sha256="", |
| requires=["insightface-antelopev2"], |
| ), |
|
|
| |
| "instantid-ip-adapter": ModelInfo( |
| id="instantid-ip-adapter", |
| name="InstantID IP-Adapter", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="ip-adapter.bin", |
| subdir="instantid", |
| description="InstantID adapter checkpoint for identity-preserving diffusion generation.", |
| license="Apache 2.0", |
| commercial_use_ok=True, |
| homepage="https://huggingface.co/InstantX/InstantID", |
| download_url="https://huggingface.co/InstantX/InstantID/resolve/main/ip-adapter.bin", |
| sha256="", |
| requires=["insightface-antelopev2"], |
| ), |
|
|
| |
| "instantid-controlnet": ModelInfo( |
| id="instantid-controlnet", |
| name="InstantID ControlNet", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="diffusion_pytorch_model.safetensors", |
| subdir="controlnet/InstantID", |
| description="InstantID ControlNet for facial keypoint guidance during generation.", |
| license="Apache 2.0", |
| commercial_use_ok=True, |
| homepage="https://huggingface.co/InstantX/InstantID", |
| download_url="https://huggingface.co/InstantX/InstantID/resolve/main/ControlNetModel/diffusion_pytorch_model.safetensors", |
| sha256="", |
| requires=["instantid-ip-adapter"], |
| ), |
|
|
| |
| "openpose-controlnet-sdxl": ModelInfo( |
| id="openpose-controlnet-sdxl", |
| name="OpenPose ControlNet (SDXL)", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="diffusion_pytorch_model.safetensors", |
| subdir="controlnet/thibaud-openpose-sdxl-1.0", |
| description="OpenPose ControlNet for SDXL enabling pose-guided body generation. Safetensors conversion by dimitribarbot.", |
| license="CreativeML Open RAIL-M", |
| commercial_use_ok=True, |
| homepage="https://huggingface.co/thibaud/controlnet-openpose-sdxl-1.0", |
| download_url="https://huggingface.co/dimitribarbot/controlnet-openpose-sdxl-1.0-safetensors/resolve/main/diffusion_pytorch_model.safetensors", |
| sha256="", |
| requires=["instantid-controlnet"], |
| ), |
|
|
| |
| "photomaker-v2": ModelInfo( |
| id="photomaker-v2", |
| name="PhotoMaker V2", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="photomaker-v2.bin", |
| subdir="photomaker", |
| description="PhotoMaker V2 identity-preserving encoder for SDXL-compatible workflows.", |
| license="Apache 2.0", |
| commercial_use_ok=True, |
| homepage="https://huggingface.co/TencentARC/PhotoMaker-V2", |
| download_url="https://huggingface.co/TencentARC/PhotoMaker-V2/resolve/main/photomaker-v2.bin", |
| sha256="", |
| requires=[], |
| ), |
|
|
| |
| "pulid-flux": ModelInfo( |
| id="pulid-flux", |
| name="PuLID for FLUX", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="pulid_flux_v0.9.0.safetensors", |
| subdir="pulid", |
| description="PuLID adapter for FLUX identity customization with minimal disruption.", |
| license="Apache 2.0", |
| commercial_use_ok=True, |
| homepage="https://huggingface.co/guozinan/PuLID", |
| download_url="https://huggingface.co/guozinan/PuLID/resolve/main/pulid_flux_v0.9.0.safetensors", |
| sha256="", |
| requires=["insightface-antelopev2"], |
| ), |
|
|
| |
| "ip-adapter-faceid-plusv2": ModelInfo( |
| id="ip-adapter-faceid-plusv2", |
| name="IP-Adapter FaceID Plus V2 (SDXL)", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="ip-adapter-faceid-plusv2_sdxl.bin", |
| subdir="ipadapter", |
| description="Face-conditioned generation adapter for SDXL. Non-commercial license.", |
| license="Non-Commercial (h94)", |
| commercial_use_ok=False, |
| homepage="https://huggingface.co/h94/IP-Adapter-FaceID", |
| download_url="https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sdxl.bin", |
| sha256="", |
| requires=["insightface-antelopev2"], |
| ), |
|
|
| |
| "stylegan2-ffhq-256": ModelInfo( |
| id="stylegan2-ffhq-256", |
| name="StyleGAN2 FFHQ 256", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="stylegan2-ffhq-256x256.pkl", |
| subdir="avatar", |
| description="Fast random-face generator (FFHQ 256). Non-commercial (NVIDIA).", |
| license="NVIDIA Source Code License (Non-commercial)", |
| commercial_use_ok=False, |
| homepage="https://github.com/NVlabs/stylegan2", |
| download_url="https://api.ngc.nvidia.com/v2/models/nvidia/research/stylegan2/versions/1/files/stylegan2-ffhq-256x256.pkl", |
| sha256="", |
| requires=[], |
| ), |
|
|
| |
| "stylegan2-ffhq-1024": ModelInfo( |
| id="stylegan2-ffhq-1024", |
| name="StyleGAN2 FFHQ 1024", |
| category=ModelCategory.AVATAR_GENERATION, |
| filename="stylegan2-ffhq-1024x1024.pkl", |
| subdir="avatar", |
| description="High-quality random-face generator (FFHQ 1024). Non-commercial (NVIDIA).", |
| license="NVIDIA Source Code License (Non-commercial)", |
| commercial_use_ok=False, |
| homepage="https://github.com/NVlabs/stylegan2", |
| download_url="https://api.ngc.nvidia.com/v2/models/nvidia/research/stylegan2/versions/1/files/stylegan2-ffhq-1024x1024.pkl", |
| sha256="", |
| requires=[], |
| ), |
| } |
|
|
|
|
| |
| ALL_MODELS: Dict[ModelCategory, Dict[str, ModelInfo]] = { |
| ModelCategory.UPSCALE: UPSCALE_MODELS, |
| ModelCategory.FACE_RESTORE: FACE_RESTORE_MODELS, |
| ModelCategory.BACKGROUND: BACKGROUND_MODELS, |
| |
| ModelCategory.AVATAR_GENERATION: AVATAR_GENERATION_MODELS, |
| } |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class EnhanceModeConfig: |
| """Configuration for an enhance mode.""" |
| mode: str |
| name: str |
| description: str |
| workflow: str |
| model_category: ModelCategory |
| default_model_id: str |
| param_name: str = "upscale_model" |
|
|
|
|
| |
| ENHANCE_MODES: Dict[str, EnhanceModeConfig] = { |
| "photo": EnhanceModeConfig( |
| mode="photo", |
| name="Photo Enhancement", |
| description="Natural photo enhancement with texture recovery", |
| workflow="upscale", |
| model_category=ModelCategory.UPSCALE, |
| default_model_id="4x-UltraSharp", |
| param_name="upscale_model", |
| ), |
| "restore": EnhanceModeConfig( |
| mode="restore", |
| name="Restoration", |
| description="Remove artifacts and mild blur", |
| workflow="upscale", |
| model_category=ModelCategory.UPSCALE, |
| default_model_id="4x-UltraSharp", |
| param_name="upscale_model", |
| ), |
| "faces": EnhanceModeConfig( |
| mode="faces", |
| name="Face Restoration", |
| description="Restore and enhance faces using FaceDetailer", |
| workflow="fix_faces_facedetailer", |
| model_category=ModelCategory.FACE_RESTORE, |
| default_model_id="GFPGANv1.4", |
| param_name="model_name", |
| ), |
| } |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class EditModelPreferences: |
| """User preferences for edit models.""" |
| |
| upscale_model: str = "4x-UltraSharp" |
| photo_model: str = "4x-UltraSharp" |
| restore_model: str = "4x-UltraSharp" |
| faces_model: str = "GFPGANv1.4" |
|
|
| def get_model_for_mode(self, mode: str) -> str: |
| """Get the selected model ID for a given mode.""" |
| mapping = { |
| "upscale": self.upscale_model, |
| "photo": self.photo_model, |
| "restore": self.restore_model, |
| "faces": self.faces_model, |
| } |
| return mapping.get(mode, self.upscale_model) |
|
|
| def set_model_for_mode(self, mode: str, model_id: str) -> None: |
| """Set the selected model ID for a given mode.""" |
| if mode == "upscale": |
| self.upscale_model = model_id |
| elif mode == "photo": |
| self.photo_model = model_id |
| elif mode == "restore": |
| self.restore_model = model_id |
| elif mode == "faces": |
| self.faces_model = model_id |
|
|
|
|
| |
| _preferences = EditModelPreferences() |
|
|
|
|
| def get_preferences() -> EditModelPreferences: |
| """Get current model preferences.""" |
| return _preferences |
|
|
|
|
| def set_preferences(prefs: EditModelPreferences) -> None: |
| """Set model preferences.""" |
| global _preferences |
| _preferences = prefs |
|
|
|
|
| |
| |
| |
|
|
| def get_installed_models(category: ModelCategory) -> List[ModelInfo]: |
| """Get list of installed models in a category.""" |
| models = ALL_MODELS.get(category, {}) |
| return [m for m in models.values() if m.installed] |
|
|
|
|
| def get_all_models(category: ModelCategory) -> List[ModelInfo]: |
| """Get all models in a category (installed or not).""" |
| return list(ALL_MODELS.get(category, {}).values()) |
|
|
|
|
| def get_model_info(model_id: str) -> Optional[ModelInfo]: |
| """Get model info by ID, searching all categories.""" |
| for category_models in ALL_MODELS.values(): |
| if model_id in category_models: |
| return category_models[model_id] |
| return None |
|
|
|
|
| def get_default_model(category: ModelCategory) -> Optional[ModelInfo]: |
| """Get the default model for a category.""" |
| models = ALL_MODELS.get(category, {}) |
| for model in models.values(): |
| if model.is_default: |
| return model |
| |
| return next(iter(models.values()), None) if models else None |
|
|
|
|
| def get_available_model(category: ModelCategory, preferred_id: Optional[str] = None) -> Optional[ModelInfo]: |
| """ |
| Get an available model, preferring the specified one. |
| |
| Priority: |
| 1. Preferred model if installed |
| 2. Default model if installed |
| 3. Any installed model |
| 4. None if nothing installed |
| """ |
| |
| if preferred_id: |
| model = get_model_info(preferred_id) |
| if model and model.installed: |
| return model |
|
|
| |
| default = get_default_model(category) |
| if default and default.installed: |
| return default |
|
|
| |
| installed = get_installed_models(category) |
| if installed: |
| return installed[0] |
|
|
| return None |
|
|
|
|
| |
| |
| |
|
|
| def get_upscale_model() -> tuple[Optional[str], Optional[str]]: |
| """ |
| Get the upscale model to use. |
| |
| Returns: |
| (model_filename, error_message) - filename if available, error if not |
| """ |
| prefs = get_preferences() |
| model = get_available_model(ModelCategory.UPSCALE, prefs.upscale_model) |
|
|
| if model: |
| return model.filename, None |
|
|
| |
| all_models = get_all_models(ModelCategory.UPSCALE) |
| model_names = [m.name for m in all_models] |
| return None, f"No upscale model installed. Please install one of: {', '.join(model_names)}" |
|
|
|
|
| def get_enhance_model(mode: str) -> tuple[Optional[str], Optional[str], Optional[EnhanceModeConfig]]: |
| """ |
| Get the model to use for an enhance mode. |
| |
| Args: |
| mode: "photo", "restore", or "faces" |
| |
| Returns: |
| (model_filename, error_message, mode_config) |
| """ |
| mode_config = ENHANCE_MODES.get(mode) |
| if not mode_config: |
| return None, f"Unknown enhance mode: {mode}", None |
|
|
| prefs = get_preferences() |
| preferred = prefs.get_model_for_mode(mode) |
| model = get_available_model(mode_config.model_category, preferred) |
|
|
| if model: |
| return model.filename, None, mode_config |
|
|
| |
| all_models = get_all_models(mode_config.model_category) |
| model_names = [m.name for m in all_models] |
| return None, f"No {mode_config.name} model installed. Please install one of: {', '.join(model_names)}", mode_config |
|
|
|
|
| def get_face_restore_model() -> tuple[Optional[str], Optional[str]]: |
| """ |
| Get the face restoration model to use. |
| |
| Returns: |
| (model_filename, error_message) |
| """ |
| prefs = get_preferences() |
| model = get_available_model(ModelCategory.FACE_RESTORE, prefs.faces_model) |
|
|
| if model: |
| return model.filename, None |
|
|
| all_models = get_all_models(ModelCategory.FACE_RESTORE) |
| model_names = [m.name for m in all_models] |
| return None, f"No face restoration model installed. Please install one of: {', '.join(model_names)}" |
|
|
|
|
| |
| |
| |
|
|
| def get_edit_models_status() -> Dict[str, Any]: |
| """ |
| Get comprehensive status of all edit models. |
| |
| Returns a dict suitable for API response with installed/available models. |
| """ |
| |
| try: |
| from .face_restore import face_restore_ready |
| comfyui_ok, comfyui_reason = face_restore_ready() |
| except Exception: |
| comfyui_ok, comfyui_reason = False, "Import error" |
|
|
| status: Dict[str, Any] = { |
| "upscale": { |
| "installed": [], |
| "available": [], |
| "selected": get_preferences().upscale_model, |
| "default": None, |
| }, |
| "enhance": { |
| "photo": { |
| "installed": [], |
| "available": [], |
| "selected": get_preferences().photo_model, |
| }, |
| "restore": { |
| "installed": [], |
| "available": [], |
| "selected": get_preferences().restore_model, |
| }, |
| "faces": { |
| "installed": [], |
| "available": [], |
| "selected": get_preferences().faces_model, |
| "comfyui_ready": comfyui_ok, |
| "comfyui_status": comfyui_reason, |
| }, |
| }, |
| } |
|
|
| |
| for model in get_all_models(ModelCategory.UPSCALE): |
| model_info = { |
| "id": model.id, |
| "name": model.name, |
| "description": model.description, |
| "filename": model.filename, |
| "installed": model.installed, |
| "is_default": model.is_default, |
| } |
| status["upscale"]["available"].append(model_info) |
| if model.installed: |
| status["upscale"]["installed"].append(model.id) |
| if model.is_default: |
| status["upscale"]["default"] = model.id |
|
|
| |
| for model in get_all_models(ModelCategory.FACE_RESTORE): |
| model_info = { |
| "id": model.id, |
| "name": model.name, |
| "description": model.description, |
| "filename": model.filename, |
| "installed": model.installed, |
| "is_default": model.is_default, |
| } |
| status["enhance"]["faces"]["available"].append(model_info) |
| if model.installed: |
| status["enhance"]["faces"]["installed"].append(model.id) |
|
|
| |
| status["enhance"]["photo"]["available"] = status["upscale"]["available"] |
| status["enhance"]["photo"]["installed"] = status["upscale"]["installed"] |
| status["enhance"]["restore"]["available"] = status["upscale"]["available"] |
| status["enhance"]["restore"]["installed"] = status["upscale"]["installed"] |
|
|
| return status |
|
|
|
|
| def set_model_preference(mode: str, model_id: str) -> tuple[bool, Optional[str]]: |
| """ |
| Set the preferred model for a mode. |
| |
| Args: |
| mode: "upscale", "photo", "restore", or "faces" |
| model_id: The model ID to use |
| |
| Returns: |
| (success, error_message) |
| """ |
| |
| model = get_model_info(model_id) |
| if not model: |
| return False, f"Unknown model: {model_id}" |
|
|
| |
| mode_to_category = { |
| "upscale": ModelCategory.UPSCALE, |
| "photo": ModelCategory.UPSCALE, |
| "restore": ModelCategory.UPSCALE, |
| "faces": ModelCategory.FACE_RESTORE, |
| } |
|
|
| expected_category = mode_to_category.get(mode) |
| if not expected_category: |
| return False, f"Unknown mode: {mode}" |
|
|
| if model.category != expected_category: |
| return False, f"Model {model_id} is not compatible with mode {mode}" |
|
|
| |
| if not model.installed: |
| return False, f"Model {model.name} is not installed. Please install it first." |
|
|
| |
| prefs = get_preferences() |
| prefs.set_model_for_mode(mode, model_id) |
|
|
| return True, None |
|
|
|
|
| |
| |
| |
|
|
| def get_avatar_models_status() -> Dict[str, Any]: |
| """ |
| Return installed/available status for Avatar Generator models. |
| |
| Intended for the PersonaWizard UI to gate avatar generation modes |
| (enable/disable based on which models are downloaded). |
| This does NOT alter existing edit/enhance behavior. |
| """ |
| models = get_all_models(ModelCategory.AVATAR_GENERATION) |
|
|
| available = [] |
| installed_ids = [] |
|
|
| |
| FEATURE_MODELS: Dict[str, Dict[str, Any]] = { |
| "photo_variations": { |
| "label": "Same-Person Photo Variations", |
| "description": "Generate new photos preserving the persona's identity", |
| "required": ["insightface-antelopev2", "instantid-ip-adapter"], |
| "recommended": ["instantid-controlnet"], |
| }, |
| "outfit_generation": { |
| "label": "Outfit / Wardrobe Generation", |
| "description": "Generate outfit variations with identity preservation", |
| "required": ["insightface-antelopev2", "instantid-ip-adapter", "instantid-controlnet"], |
| "recommended": ["photomaker-v2", "pulid-flux"], |
| "recommended_note": "PhotoMaker V2 for SDXL models, PuLID for FLUX models", |
| }, |
| "face_swap": { |
| "label": "Face Swap", |
| "description": "Transfer identity onto generated or existing images", |
| "required": ["insightface-antelopev2", "insightface-inswapper-128"], |
| "recommended": [], |
| }, |
| "pose_guided_body": { |
| "label": "Pose-Guided Body Generation", |
| "description": "Generate full-body images with OpenPose control for precise posing", |
| "required": ["insightface-antelopev2", "instantid-ip-adapter", "instantid-controlnet", "openpose-controlnet-sdxl"], |
| "recommended": [], |
| }, |
| "random_faces": { |
| "label": "Random Face Generator", |
| "description": "Generate random realistic faces (non-commercial)", |
| "required": ["stylegan2-ffhq-256"], |
| "recommended": ["stylegan2-ffhq-1024"], |
| "recommended_note": "1024px version for higher quality output", |
| }, |
| } |
|
|
| |
| model_features: Dict[str, list] = {} |
| for feat_id, feat in FEATURE_MODELS.items(): |
| for mid in feat["required"]: |
| model_features.setdefault(mid, []).append({"feature": feat_id, "role": "required"}) |
| for mid in feat.get("recommended", []): |
| model_features.setdefault(mid, []).append({"feature": feat_id, "role": "recommended"}) |
|
|
| for m in models: |
| info = { |
| "id": m.id, |
| "name": m.name, |
| "description": m.description, |
| "filename": m.filename, |
| "subdir": m.subdir, |
| "installed": m.installed, |
| "license": m.license, |
| "commercial_use_ok": m.commercial_use_ok, |
| "homepage": m.homepage, |
| "download_url": m.download_url, |
| "sha256": m.sha256, |
| "requires": m.requires, |
| "is_default": m.is_default, |
| "used_by": model_features.get(m.id, []), |
| } |
| available.append(info) |
| if m.installed: |
| installed_ids.append(m.id) |
|
|
| |
| installed_set = set(installed_ids) |
| features = {} |
| for feat_id, feat in FEATURE_MODELS.items(): |
| required_ok = all(mid in installed_set for mid in feat["required"]) |
| required_missing = [mid for mid in feat["required"] if mid not in installed_set] |
| recommended_ok = all(mid in installed_set for mid in feat.get("recommended", [])) |
| features[feat_id] = { |
| "label": feat["label"], |
| "description": feat["description"], |
| "ready": required_ok, |
| "required_missing": required_missing, |
| "recommended_installed": recommended_ok, |
| "recommended_note": feat.get("recommended_note"), |
| } |
|
|
| return { |
| "category": ModelCategory.AVATAR_GENERATION.value, |
| "installed": installed_ids, |
| "available": available, |
| "defaults": [m.id for m in models if m.is_default], |
| "features": features, |
| } |
|
|