feat(recipes): add Unknown base-model filter bucket for undetermined recipes

Normalize undetermined recipe base_model to None in RecipeFormatParser
(previously ''). get_base_models now reports an "Unknown" bucket backed
by a dedicated __unknown__ marker, and the listing filter matches it
against recipes whose base model is falsy. Frontend renders the bucket
label as "Unknown" while filtering via the marker.

Tests: handler, scanner, parser, and frontend filtering.
This commit is contained in:
Will Miao
2026-08-31 09:09:53 +08:00
parent 8d46d26abe
commit 2a3c632dc5
8 changed files with 214 additions and 11 deletions
+1 -1
View File
@@ -196,7 +196,7 @@ class RecipeFormatParser(RecipeMetadataParser):
filtered_gen_params[key] = value
return {
'base_model': checkpoint['baseModel'] if checkpoint and checkpoint.get('baseModel') else recipe_metadata.get('base_model', ''),
'base_model': checkpoint['baseModel'] if checkpoint and checkpoint.get('baseModel') else (recipe_metadata.get('base_model') or None),
'loras': loras,
'gen_params': filtered_gen_params,
'tags': recipe_metadata.get('tags', []),
+16
View File
@@ -26,6 +26,7 @@ from ...services.recipes import (
RecipeValidationError,
)
from ...services.metadata_service import get_default_metadata_provider
from ...services.recipe_scanner import UNKNOWN_BASE_MODEL_FILTER
from ...utils.civitai_utils import (
build_civitai_image_page_url,
extract_civitai_image_id,
@@ -473,17 +474,32 @@ class RecipeQueryHandler:
cache = await recipe_scanner.get_cached_data()
base_model_counts: Dict[str, int] = {}
unknown_count = 0
for recipe in getattr(cache, "raw_data", []):
base_model = recipe.get("base_model")
if base_model:
base_model_counts[base_model] = (
base_model_counts.get(base_model, 0) + 1
)
else:
unknown_count += 1
sorted_models = [
{"name": model, "count": count}
for model, count in base_model_counts.items()
]
if unknown_count:
# Synthetic "Unknown" bucket for recipes whose base model could
# not be determined. `value` carries the filter marker so the
# UI can display "Unknown" without colliding with real base
# model strings.
sorted_models.append(
{
"name": "Unknown",
"value": UNKNOWN_BASE_MODEL_FILTER,
"count": unknown_count,
}
)
sorted_models.sort(key=lambda entry: entry["count"], reverse=True)
if limit > 0:
sorted_models = sorted_models[:limit]
+23 -5
View File
@@ -48,6 +48,12 @@ _CHECKPOINT_MODEL_TYPE_ALIASES = {"diffusionmodel": "diffusion_model"}
# Valid LoRA availability statuses for the recipe listing filter.
_VALID_LORA_AVAILABILITY_STATUSES = frozenset({"ready", "missing", "deleted"})
# Filter marker for recipes whose base model could not be determined
# (base_model is None or empty). The UI displays "Unknown" for this bucket;
# the marker keeps the semantics explicit and disjoint from any real base
# model string.
UNKNOWN_BASE_MODEL_FILTER = "__unknown__"
class RecipeScanner:
"""Service for scanning and managing recipe images"""
@@ -3530,11 +3536,23 @@ class RecipeScanner:
if filters:
# Filter by base model
if "base_model" in filters and filters["base_model"]:
filtered_data = [
item
for item in filtered_data
if item.get("base_model", "") in filters["base_model"]
]
base_model_filter = filters["base_model"]
if UNKNOWN_BASE_MODEL_FILTER in base_model_filter:
# The unknown bucket matches recipes whose base model
# could not be determined (None/empty); real base
# models in the list still match by exact name.
filtered_data = [
item
for item in filtered_data
if not item.get("base_model")
or item.get("base_model") in base_model_filter
]
else:
filtered_data = [
item
for item in filtered_data
if item.get("base_model", "") in base_model_filter
]
# Filter by favorite
if "favorite" in filters and filters["favorite"]: