feat(backend): CivitAI download support for other model types with subtype routing

This commit is contained in:
Will Miao
2026-09-12 15:56:47 +08:00
parent 57729375b6
commit f2a7297cb9
18 changed files with 1492 additions and 16 deletions
@@ -7,7 +7,11 @@ import logging
from aiohttp import web
from ...services.download_routing import is_diffusion_model_download
from ...services.download_routing import (
is_diffusion_model_download,
resolve_other_download_sub_type,
)
from ...utils.constants import VALID_OTHER_CIVITAI_TYPES
logger = logging.getLogger(__name__)
@@ -31,6 +35,7 @@ class DownloadRoutingHandler:
model_type = payload.get("model_type", "")
base_model = payload.get("base_model") or ""
file_types = payload.get("file_types") or []
selected_file_type = payload.get("selected_file_type")
if not isinstance(model_type, str) or not model_type:
return web.json_response(
@@ -44,6 +49,25 @@ class DownloadRoutingHandler:
},
status=400,
)
if selected_file_type is not None and not isinstance(selected_file_type, str):
return web.json_response(
{"success": False, "error": "selected_file_type must be a string"},
status=400,
)
if model_type.lower() in VALID_OTHER_CIVITAI_TYPES:
sub_type = resolve_other_download_sub_type(
model_type,
file_types=(str(t) for t in file_types),
selected_file_type=selected_file_type,
)
return web.json_response(
{
"success": True,
"root_kind": "other",
"sub_type": sub_type,
}
)
is_diffusion = is_diffusion_model_download(
model_type,
+26
View File
@@ -53,6 +53,7 @@ from ...utils.constants import (
PREVIEW_EXTENSIONS,
SUPPORTED_MEDIA_EXTENSIONS,
VALID_LORA_TYPES,
VALID_OTHER_CIVITAI_TYPES,
)
from .hf_handlers import HfHandler
from .agent_handlers import AgentHandler
@@ -2068,6 +2069,7 @@ class ServiceRegistryAdapter:
get_embedding_scanner: Callable[[], Awaitable[Any]]
get_downloaded_version_history_service: Callable[[], Awaitable[Any]]
get_backup_service: Callable[[], Awaitable[Any]] = _noop_backup_service
get_other_scanner: Callable[[], Awaitable[Any]] = ServiceRegistry.get_other_scanner
class ModelLibraryHandler:
@@ -2789,12 +2791,30 @@ class ModelLibraryHandler:
model_type.lower() for model_type in CIVITAI_USER_MODEL_TYPES
}
lora_type_aliases = {model_type.lower() for model_type in VALID_LORA_TYPES}
other_type_aliases = {
model_type.lower() for model_type in VALID_OTHER_CIVITAI_TYPES
}
# Acquire the other scanner lazily so adapters without it only
# fail when the payload actually contains other-type models.
needs_other_scanner = any(
isinstance(model, dict)
and str(model.get("type", "")).lower() in other_type_aliases
for model in models
)
other_scanner = None
if needs_other_scanner:
other_scanner = await self._service_registry.get_other_scanner()
type_scanner_map: Dict[str, Any] = {
**{alias: lora_scanner for alias in lora_type_aliases},
"checkpoint": checkpoint_scanner,
"textualinversion": embedding_scanner,
}
if other_scanner is not None:
type_scanner_map.update(
{alias: other_scanner for alias in other_type_aliases}
)
versions: list[dict[str, Any]] = []
history_service = await self._get_download_history_service()
@@ -2818,12 +2838,17 @@ class ModelLibraryHandler:
"embedding",
model_ids,
)
other_downloaded = await history_service.get_downloaded_version_ids_bulk(
"other",
model_ids,
)
downloaded_version_map: Dict[str, Dict[int, set[int]]] = {
"lora": lora_downloaded,
"locon": lora_downloaded,
"dora": lora_downloaded,
"checkpoint": checkpoint_downloaded,
"textualinversion": embedding_downloaded,
**{alias: other_downloaded for alias in VALID_OTHER_CIVITAI_TYPES},
}
for model in models:
if not isinstance(model, dict):
@@ -3982,6 +4007,7 @@ def build_service_registry_adapter() -> ServiceRegistryAdapter:
get_lora_scanner=ServiceRegistry.get_lora_scanner,
get_checkpoint_scanner=ServiceRegistry.get_checkpoint_scanner,
get_embedding_scanner=ServiceRegistry.get_embedding_scanner,
get_other_scanner=ServiceRegistry.get_other_scanner,
get_downloaded_version_history_service=ServiceRegistry.get_downloaded_version_history_service,
get_backup_service=ServiceRegistry.get_backup_service,
)
+32 -2
View File
@@ -1,12 +1,13 @@
import logging
from typing import Any, Dict
from typing import Any, Dict, List
from aiohttp import web
from .base_model_routes import BaseModelRoutes
from .model_route_registrar import ModelRouteRegistrar
from ..config import config
from ..services.other_model_service import OtherModelService
from ..services.service_registry import ServiceRegistry
from ..utils.constants import VALID_OTHER_CIVITAI_TYPES
from ..utils.constants import OTHER_MODEL_FOLDER_SUBTYPES, VALID_OTHER_CIVITAI_TYPES
logger = logging.getLogger(__name__)
@@ -41,6 +42,9 @@ class OtherRoutes(BaseModelRoutes):
"""Setup Other-model-specific routes"""
# Other-model info by name
registrar.add_prefixed_route('GET', '/api/lm/{prefix}/info/{name}', prefix, self.get_other_model_info)
# Other-model roots grouped by sub_type (text_encoders + legacy clip
# are aggregated under text_encoder)
registrar.add_prefixed_route('GET', '/api/lm/{prefix}/roots_by_subtype', prefix, self.get_roots_by_subtype)
def _validate_civitai_model_type(self, model_type: str) -> bool:
"""Validate CivitAI model type for other models.
@@ -58,6 +62,32 @@ class OtherRoutes(BaseModelRoutes):
"""Parse other-model-specific parameters (none in Phase 1)."""
return {}
async def get_roots_by_subtype(self, request: web.Request) -> web.Response:
"""Return other-model roots grouped by sub_type.
Aggregates the per-folder_paths-key roots from config
(``text_encoders`` and the legacy ``clip`` key both land under
``text_encoder``).
"""
try:
roots_by_subtype: Dict[str, List[str]] = {}
for key, roots in (config.other_folder_roots or {}).items():
sub_type = OTHER_MODEL_FOLDER_SUBTYPES.get(key)
if not sub_type:
continue
bucket = roots_by_subtype.setdefault(sub_type, [])
for root in roots:
if root and root not in bucket:
bucket.append(root)
return web.json_response(
{"success": True, "roots_by_subtype": roots_by_subtype}
)
except Exception as e:
logger.error(f"Error getting other roots by sub_type: {e}", exc_info=True)
return web.json_response(
{"success": False, "error": str(e)}, status=500
)
async def get_other_model_info(self, request: web.Request) -> web.Response:
"""Get detailed information for a specific other model by name"""
try:
+88 -5
View File
@@ -17,12 +17,18 @@ from dataclasses import dataclass, field
import uuid
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, cast
from urllib.parse import urlparse
from ..utils.models import LoraMetadata, CheckpointMetadata, EmbeddingMetadata
from ..utils.models import (
LoraMetadata,
CheckpointMetadata,
EmbeddingMetadata,
OtherModelMetadata,
)
from ..utils.constants import (
CARD_PREVIEW_WIDTH,
MODEL_WEIGHT_FILE_TYPES,
SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS,
VALID_LORA_TYPES,
VALID_OTHER_CIVITAI_TYPES,
)
from ..utils.civitai_utils import normalize_civitai_download_url, rewrite_preview_url
from ..utils.file_utils import calculate_sha256, calculate_autov3
@@ -31,7 +37,7 @@ from ..utils.utils import sanitize_folder_name
from ..utils.exif_utils import ExifUtils
from ..utils.metadata_manager import MetadataManager
from .service_registry import ServiceRegistry
from .download_routing import is_diffusion_model_download
from .download_routing import is_diffusion_model_download, resolve_other_download_sub_type
from .settings_manager import get_settings_manager
from .metadata_service import get_default_metadata_provider, get_metadata_provider
from .downloader import get_downloader, DownloadProgress, DownloadStreamControl
@@ -228,12 +234,21 @@ class DownloadManager:
return False
async def _get_scanner_for_model_type(self, model_type: str):
"""Return the scanner responsible for the given model type."""
"""Return the scanner responsible for the given model type.
Every supported type resolves explicitly an unknown type must never
fall through to the lora scanner (an "other" download would silently
dedupe against the lora library).
"""
if model_type == "checkpoint":
return await self._get_checkpoint_scanner()
if model_type == "embedding":
return await ServiceRegistry.get_embedding_scanner()
return await self._get_lora_scanner()
if model_type == "other":
return await ServiceRegistry.get_other_scanner()
if model_type == "lora":
return await self._get_lora_scanner()
raise ValueError(f'Unknown model type "{model_type}"')
@staticmethod
def _resolve_target_file(
@@ -978,6 +993,8 @@ class DownloadManager:
return CheckpointMetadata.from_civitai_info(version_info, file_info, save_path)
if model_type == "embedding":
return EmbeddingMetadata.from_civitai_info(version_info, file_info, save_path)
if model_type == "other":
return OtherModelMetadata.from_civitai_info(version_info, file_info, save_path)
return LoraMetadata.from_civitai_info(version_info, file_info, save_path)
def _resolve_save_path_from_persisted_record(self, record: Dict[str, Any]) -> Optional[str]:
@@ -1438,6 +1455,7 @@ class DownloadManager:
lora_scanner = await self._get_lora_scanner()
checkpoint_scanner = await self._get_checkpoint_scanner()
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
other_scanner = await ServiceRegistry.get_other_scanner()
# Check lora scanner first
if await lora_scanner.check_model_version_exists(model_version_id):
@@ -1462,6 +1480,13 @@ class DownloadManager:
"error": "Model version already exists in embedding library",
}
# Check other scanner
if await other_scanner.check_model_version_exists(model_version_id):
return {
"success": False,
"error": "Model version already exists in other library",
}
# Use CivArchive provider directly when source is 'civarchive'
# This prioritizes CivArchive metadata (with mirror availability info) over Civitai
if source == "civarchive":
@@ -1500,6 +1525,8 @@ class DownloadManager:
model_type = "lora"
elif model_type_from_info == "textualinversion":
model_type = "embedding"
elif model_type_from_info in VALID_OTHER_CIVITAI_TYPES:
model_type = "other"
else:
return {
"success": False,
@@ -1686,6 +1713,13 @@ class DownloadManager:
"success": False,
"error": "Model version already exists in embedding library",
}
elif model_type == "other":
other_scanner = await ServiceRegistry.get_other_scanner()
if await other_scanner.check_model_version_exists(version_id):
return {
"success": False,
"error": "Model version already exists in other library",
}
# Handle use_default_paths
if use_default_paths:
@@ -1725,6 +1759,45 @@ class DownloadManager:
"error": "Default embedding root path not set in settings",
}
save_dir = default_path
elif model_type == "other":
other_sub_type = resolve_other_download_sub_type(
model_type_from_info,
file_types=(
f.get("type", "")
for f in version_info.get("files", [])
if isinstance(f, dict)
),
selected_file_type=(
target_file.get("type") if explicit_file else None
),
)
default_other_roots = (
settings_manager.get("default_other_roots") or {}
)
default_path = (
default_other_roots.get(other_sub_type)
if other_sub_type
else None
)
if not isinstance(default_path, str) or not default_path:
if other_sub_type:
detail = (
f"No default root configured for other-model "
f"sub-type '{other_sub_type}'"
)
else:
detail = (
"Could not determine the other-model sub-type "
"from the model metadata"
)
return {
"success": False,
"error": (
f"{detail}. Please pick a destination folder "
f"explicitly instead of using default paths."
),
}
save_dir = default_path
# Calculate relative path using template
relative_path = self._calculate_relative_path(version_info, model_type)
@@ -1921,6 +1994,11 @@ class DownloadManager:
version_info, file_info, save_path
)
logger.info(f"Creating EmbeddingMetadata for {file_name}")
elif model_type == "other":
metadata = OtherModelMetadata.from_civitai_info(
version_info, file_info, save_path
)
logger.info(f"Creating OtherModelMetadata for {file_name}")
else:
return {
"success": False,
@@ -2133,6 +2211,8 @@ class DownloadManager:
scanner = await self._get_checkpoint_scanner()
elif model_type == "embedding":
scanner = await ServiceRegistry.get_embedding_scanner()
elif model_type == "other":
scanner = await ServiceRegistry.get_other_scanner()
except Exception as exc:
logger.debug("Failed to acquire scanner for %s models: %s", model_type, exc)
@@ -2629,6 +2709,9 @@ class DownloadManager:
elif model_type == "embedding":
scanner = await ServiceRegistry.get_embedding_scanner()
logger.info(f"Updating embedding cache for {actual_file_paths[0]}")
elif model_type == "other":
scanner = await ServiceRegistry.get_other_scanner()
logger.info(f"Updating other-model cache for {actual_file_paths[0]}")
adjust_cached_entry = (
getattr(scanner, "adjust_cached_entry", None)
@@ -2718,7 +2801,7 @@ class DownloadManager:
return {"success": False, "error": str(e)}
def _get_supported_extensions_for_type(self, model_type: str) -> Set[str]:
if model_type == "checkpoint":
if model_type in ("checkpoint", "other"):
return {
".ckpt",
".pt",
+52 -2
View File
@@ -10,9 +10,13 @@ two can never disagree.
from __future__ import annotations
import logging
from typing import Iterable
from typing import Iterable, Optional
from ..utils.constants import DIFFUSION_MODEL_BASE_MODELS
from ..utils.constants import (
CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE,
CIVITAI_TYPE_TO_OTHER_SUB_TYPE,
DIFFUSION_MODEL_BASE_MODELS,
)
logger = logging.getLogger(__name__)
@@ -51,3 +55,49 @@ def is_diffusion_model_download(
return True
return False
def resolve_other_download_sub_type(
civitai_model_type: str,
file_types: Iterable[str] = (),
selected_file_type: Optional[str] = None,
) -> Optional[str]:
"""Resolve the "other"-page sub_type for a download.
Fixed priority (locked design, docs/plans/other-models-page.md §9.2):
1. Explicit user file pick when the picked file's type maps, it wins
even when model.type maps to something else.
2. model.type via CIVITAI_TYPE_TO_OTHER_SUB_TYPE.
3. file.type fallback only when model.type maps to nothing. Must NOT
override a mapped model.type: checkpoint models routinely bundle
VAE/Text Encoder component files.
4. Still undecidable -> None (caller must ask the user for a folder).
"""
if selected_file_type:
mapped = CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE.get(selected_file_type)
if mapped:
logger.info(
"Explicit file pick type '%s' routes other download to '%s'",
selected_file_type,
mapped,
)
return mapped
normalized_model_type = (civitai_model_type or "").strip().lower()
mapped = CIVITAI_TYPE_TO_OTHER_SUB_TYPE.get(normalized_model_type)
if mapped:
return mapped
for file_type in file_types:
mapped = CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE.get(file_type)
if mapped:
logger.info(
"model.type '%s' unmapped; file type '%s' routes other download to '%s'",
civitai_model_type,
file_type,
mapped,
)
return mapped
return None
+114
View File
@@ -27,7 +27,9 @@ from platformdirs import user_config_dir
from ..utils.constants import (
DEFAULT_HASH_CHUNK_SIZE_MB,
DEFAULT_PRIORITY_TAG_CONFIG,
OTHER_MODEL_FOLDER_SUBTYPES,
SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS,
VALID_OTHER_SUB_TYPES,
)
from ..utils.preview_selection import VALID_MATURE_BLUR_LEVELS
from ..utils.settings_paths import (
@@ -83,6 +85,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
"default_checkpoint_root": "",
"default_unet_root": "",
"default_embedding_root": "",
"default_other_roots": {},
"recipes_path": "",
"base_model_path_mappings": {},
"download_path_templates": {},
@@ -309,6 +312,7 @@ class SettingsManager:
default_checkpoint_root=merged.get("default_checkpoint_root"),
default_unet_root=merged.get("default_unet_root"),
default_embedding_root=merged.get("default_embedding_root"),
default_other_roots=merged.get("default_other_roots"),
recipes_path=merged.get("recipes_path"),
)
}
@@ -443,6 +447,7 @@ class SettingsManager:
),
default_unet_root=self.settings.get("default_unet_root", ""),
default_embedding_root=self.settings.get("default_embedding_root", ""),
default_other_roots=self.settings.get("default_other_roots"),
recipes_path=self.settings.get("recipes_path", ""),
)
libraries = {library_name: library_payload}
@@ -494,6 +499,7 @@ class SettingsManager:
default_checkpoint_root=data.get("default_checkpoint_root"),
default_unet_root=data.get("default_unet_root"),
default_embedding_root=data.get("default_embedding_root"),
default_other_roots=data.get("default_other_roots"),
recipes_path=data.get("recipes_path"),
metadata=data.get("metadata"),
base=data,
@@ -541,6 +547,9 @@ class SettingsManager:
self.settings["default_embedding_root"] = active_library.get(
"default_embedding_root", ""
)
self.settings["default_other_roots"] = self._normalize_default_other_roots(
active_library.get("default_other_roots", {})
)
self.settings["recipes_path"] = active_library.get("recipes_path", "")
if save:
@@ -558,6 +567,7 @@ class SettingsManager:
default_checkpoint_root: Optional[str] = None,
default_unet_root: Optional[str] = None,
default_embedding_root: Optional[str] = None,
default_other_roots: Optional[Mapping[str, str]] = None,
recipes_path: Optional[str] = None,
metadata: Optional[Mapping[str, Any]] = None,
base: Optional[Mapping[str, Any]] = None,
@@ -597,6 +607,15 @@ class SettingsManager:
else:
payload.setdefault("default_embedding_root", "")
if default_other_roots is not None:
payload["default_other_roots"] = self._normalize_default_other_roots(
default_other_roots
)
else:
payload["default_other_roots"] = self._normalize_default_other_roots(
payload.get("default_other_roots", {})
)
if recipes_path is not None:
payload["recipes_path"] = recipes_path
else:
@@ -632,6 +651,35 @@ class SettingsManager:
normalized[key] = cleaned
return normalized
def _normalize_default_other_roots(
self, value: Any, *, strict: bool = False
) -> Dict[str, str]:
"""Normalize a ``default_other_roots`` mapping ({sub_type: root path}).
Unknown sub_type keys and non-string/empty paths are dropped; with
``strict=True`` unknown sub_type keys raise instead (used by ``set()``
so typos in API payloads surface as errors).
"""
if not isinstance(value, Mapping):
if strict and value is not None:
raise ValueError("default_other_roots must be a mapping")
return {}
normalized: Dict[str, str] = {}
for sub_type, path in value.items():
if sub_type not in VALID_OTHER_SUB_TYPES:
if strict:
raise ValueError(
f"Unknown other-model sub-type '{sub_type}'; "
f"expected one of {sorted(VALID_OTHER_SUB_TYPES)}"
)
continue
if not isinstance(path, str):
continue
stripped = path.strip()
if stripped:
normalized[sub_type] = stripped
return normalized
def _has_configured_paths(self, folder_paths: Any) -> bool:
if not isinstance(folder_paths, Mapping):
return False
@@ -744,6 +792,7 @@ class SettingsManager:
default_checkpoint_root: Optional[str] = None,
default_unet_root: Optional[str] = None,
default_embedding_root: Optional[str] = None,
default_other_roots: Optional[Mapping[str, str]] = None,
recipes_path: Optional[str] = None,
) -> bool:
libraries = self.settings.get("libraries", {})
@@ -794,6 +843,14 @@ class SettingsManager:
library["default_embedding_root"] = default_embedding_root
changed = True
if default_other_roots is not None:
normalized_other_roots = self._normalize_default_other_roots(
default_other_roots
)
if library.get("default_other_roots") != normalized_other_roots:
library["default_other_roots"] = normalized_other_roots
changed = True
if recipes_path is not None and library.get("recipes_path") != recipes_path:
library["recipes_path"] = recipes_path
changed = True
@@ -894,12 +951,53 @@ class SettingsManager:
updated = _check_and_auto_set("unet", "default_unet_root") or updated
updated = _check_and_auto_set("embeddings", "default_embedding_root") or updated
# Other-model default roots: one entry per sub_type; candidates are the
# union of that sub_type's folder_paths keys (text_encoder merges the
# legacy 'clip' key with 'text_encoders').
sub_type_folder_keys: Dict[str, List[str]] = {}
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items():
sub_type_folder_keys.setdefault(sub_type, []).append(folder_key)
other_roots = self._normalize_default_other_roots(
self.settings.get("default_other_roots")
)
for sub_type in VALID_OTHER_SUB_TYPES:
candidates: List[str] = []
candidate_identities: set[str] = set()
for folder_key in sub_type_folder_keys.get(sub_type, []):
for candidate in self._get_valid_root_candidates(folder_key):
identity = _normalize_root_identity(candidate)
if identity in candidate_identities:
continue
candidate_identities.add(identity)
candidates.append(candidate)
if not candidates:
continue
current = other_roots.get(sub_type, "")
if current and _normalize_root_identity(current) in candidate_identities:
continue
other_roots[sub_type] = candidates[0]
if current:
logger.info(
"Repaired stale default_other_roots[%s] from '%s' to '%s' because it is not present in primary or extra roots",
sub_type,
current,
candidates[0],
)
else:
logger.info(
"Auto-set default_other_roots[%s] to '%s'", sub_type, candidates[0]
)
updated = True
if updated:
self.settings["default_other_roots"] = other_roots
self._update_active_library_entry(
default_lora_root=self.settings.get("default_lora_root"),
default_checkpoint_root=self.settings.get("default_checkpoint_root"),
default_unet_root=self.settings.get("default_unet_root"),
default_embedding_root=self.settings.get("default_embedding_root"),
default_other_roots=other_roots,
)
if self._bootstrap_reason == "missing":
self._needs_initial_save = True
@@ -1599,6 +1697,8 @@ class SettingsManager:
value = self.normalize_download_skip_base_models(value)
elif key == "mature_blur_level":
value = self.normalize_mature_blur_level(value)
elif key == "default_other_roots":
value = self._normalize_default_other_roots(value, strict=True)
elif key == "recipes_path":
current_recipes_dir = self._get_effective_recipes_dir()
value = self._normalize_recipes_path_value(value)
@@ -1626,6 +1726,8 @@ class SettingsManager:
self._update_active_library_entry(default_unet_root=str(value))
elif key == "default_embedding_root":
self._update_active_library_entry(default_embedding_root=str(value))
elif key == "default_other_roots":
self._update_active_library_entry(default_other_roots=value)
elif key == "recipes_path":
self._update_active_library_entry(recipes_path=str(value))
elif key == "model_name_display":
@@ -1796,6 +1898,7 @@ class SettingsManager:
"lora_scanner",
"checkpoint_scanner",
"embedding_scanner",
"other_scanner",
"recipe_scanner",
):
service = ServiceRegistry.get_service_sync(service_name)
@@ -1960,6 +2063,7 @@ class SettingsManager:
default_checkpoint_root: Optional[str] = None,
default_unet_root: Optional[str] = None,
default_embedding_root: Optional[str] = None,
default_other_roots: Optional[Mapping[str, str]] = None,
recipes_path: Optional[str] = None,
metadata: Optional[Mapping[str, Any]] = None,
activate: bool = False,
@@ -2004,6 +2108,11 @@ class SettingsManager:
if default_embedding_root is not None
else existing.get("default_embedding_root")
),
default_other_roots=(
default_other_roots
if default_other_roots is not None
else existing.get("default_other_roots")
),
recipes_path=(
recipes_path
if recipes_path is not None
@@ -2036,6 +2145,7 @@ class SettingsManager:
default_checkpoint_root: str = "",
default_unet_root: str = "",
default_embedding_root: str = "",
default_other_roots: Optional[Mapping[str, str]] = None,
recipes_path: str = "",
metadata: Optional[Mapping[str, Any]] = None,
activate: bool = False,
@@ -2054,6 +2164,7 @@ class SettingsManager:
default_checkpoint_root=default_checkpoint_root,
default_unet_root=default_unet_root,
default_embedding_root=default_embedding_root,
default_other_roots=default_other_roots,
recipes_path=recipes_path,
metadata=metadata,
activate=activate,
@@ -2114,6 +2225,7 @@ class SettingsManager:
default_checkpoint_root: Optional[str] = None,
default_unet_root: Optional[str] = None,
default_embedding_root: Optional[str] = None,
default_other_roots: Optional[Mapping[str, str]] = None,
recipes_path: Optional[str] = None,
) -> None:
"""Update folder paths for the active library."""
@@ -2127,6 +2239,7 @@ class SettingsManager:
default_checkpoint_root=default_checkpoint_root,
default_unet_root=default_unet_root,
default_embedding_root=default_embedding_root,
default_other_roots=default_other_roots,
recipes_path=recipes_path,
activate=True,
)
@@ -2151,6 +2264,7 @@ class SettingsManager:
"lora_scanner",
"checkpoint_scanner",
"embedding_scanner",
"other_scanner",
"recipe_scanner",
"model_update_service",
):
+15
View File
@@ -126,6 +126,20 @@ CIVITAI_TYPE_TO_OTHER_SUB_TYPE = {
"controlnet": "controlnet",
}
# CivitAI ModelFile.type values -> internal sub_type for the "other" model
# page. Used for download routing only, and strictly as an explicit user file
# pick or a fallback when model.type maps to nothing — checkpoint models
# routinely bundle VAE/Text Encoder component files, so file types must never
# override a mapped model.type.
CIVITAI_FILE_TYPE_TO_OTHER_SUB_TYPE = {
"VAE": "vae",
"Upscaler": "upscaler",
"Text Encoder": "text_encoder",
"Vision Encoder": "clip_vision",
"CLIPVision": "clip_vision",
"ControlNet": "controlnet",
}
# Backward compatibility alias
VALID_LORA_TYPES = VALID_LORA_SUB_TYPES
@@ -134,6 +148,7 @@ CIVITAI_USER_MODEL_TYPES = [
*VALID_LORA_TYPES,
"textualinversion",
"checkpoint",
*sorted(VALID_OTHER_CIVITAI_TYPES),
]
# Default chunk size in megabytes used for hashing large files.
@@ -420,6 +420,10 @@ class DownloadManager:
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
scanners.append(("embedding", embedding_scanner))
if "other" in model_types:
other_scanner = await ServiceRegistry.get_other_scanner()
scanners.append(("other", other_scanner))
# Load progress file to check processed models (async to avoid blocking)
settings_manager = get_settings_manager()
active_library = settings_manager.get_active_library_name()
@@ -600,6 +604,10 @@ class DownloadManager:
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
scanners.append(("embedding", embedding_scanner))
if "other" in model_types:
other_scanner = await ServiceRegistry.get_other_scanner()
scanners.append(("other", other_scanner))
# Get all models
all_models = []
for scanner_type, scanner in scanners:
@@ -1098,6 +1106,10 @@ class DownloadManager:
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
scanners.append(("embedding", embedding_scanner))
if "other" in model_types:
other_scanner = await ServiceRegistry.get_other_scanner()
scanners.append(("other", other_scanner))
# Find the specified models
models_to_process = []
for scanner_type, scanner in scanners:
+2 -1
View File
@@ -340,7 +340,8 @@ class OtherModelMetadata(BaseModelMetadata):
sha256_value = (file_info.get("hashes") or {}).get("SHA256", "").lower()
# Map the CivitAI model type onto our sub_types; unknown types keep the
# placeholder until the scanner re-derives sub_type from the location.
civitai_type = str(version_info.get("type", "") or "").lower()
# The type lives at version["model"]["type"], not version["type"].
civitai_type = str((version_info.get("model") or {}).get("type", "") or "").lower()
sub_type = CIVITAI_TYPE_TO_OTHER_SUB_TYPE.get(civitai_type, "vae")
# Extract tags and description if available