mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-20 12:31:27 -03:00
refactor(services): unify local model name matching with uniqueness and base-model guards (#1065)
Consolidate the duplicate name-matching logic into ModelScanner: find_matching_models is now the single core, using each scanner's own file_extensions for suffix stripping. get_model_info_by_name gains require_unique/base_model kwargs while legacy route behavior is kept byte-identical. reconnect_lora passes the recipe base model as a guard and distinguishes ambiguous, base-model-mismatched, and missing LoRAs in its error messages.
This commit is contained in:
@@ -17,6 +17,7 @@ from py.services.recipes.errors import (
|
||||
RecipeValidationError,
|
||||
)
|
||||
from py.services.recipes.persistence_service import RecipePersistenceService
|
||||
from py.services.model_scanner import ModelScanner
|
||||
from py.recipes.parsers.civitai_image import CivitaiApiMetadataParser
|
||||
from py.utils.exif_utils import ExifUtils
|
||||
|
||||
@@ -1156,3 +1157,72 @@ async def test_analyze_local_image_fingerprint_uses_sha256_normalized_hash(tmp_p
|
||||
|
||||
assert result.payload["loras"][0]["hash"] == sha256
|
||||
assert result.payload["fingerprint"] == f"{sha256}:1.0"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnect_lora_distinguishes_ambiguous_mismatched_and_missing(tmp_path):
|
||||
service = RecipePersistenceService(
|
||||
exif_utils=DummyExifUtils(),
|
||||
card_preview_width=512,
|
||||
logger=logging.getLogger("test"),
|
||||
)
|
||||
|
||||
models = [
|
||||
{
|
||||
"file_name": "style.safetensors",
|
||||
"folder": "sd15",
|
||||
"file_path": "/models/loras/sd15/style.safetensors",
|
||||
"base_model": "SD 1.5",
|
||||
},
|
||||
{
|
||||
"file_name": "style.safetensors",
|
||||
"folder": "sdxl",
|
||||
"file_path": "/models/loras/sdxl/style.safetensors",
|
||||
"base_model": "SDXL 1.0",
|
||||
},
|
||||
]
|
||||
|
||||
class DummyScanner:
|
||||
def __init__(self, recipe_path):
|
||||
self._recipe_path = recipe_path
|
||||
|
||||
async def get_recipe_json_path(self, recipe_id):
|
||||
return str(self._recipe_path)
|
||||
|
||||
async def get_local_lora(self, name, base_model=None):
|
||||
matches = ModelScanner.find_matching_models(models, name, base_model=base_model)
|
||||
return matches[0] if len(matches) == 1 else None
|
||||
|
||||
async def find_local_loras_by_name(self, name, base_model=None):
|
||||
return ModelScanner.find_matching_models(models, name, base_model=base_model)
|
||||
|
||||
def write_recipe(base_model):
|
||||
recipe_path = tmp_path / "recipe.json"
|
||||
recipe_path.write_text(
|
||||
json.dumps({"id": "r1", "base_model": base_model, "loras": []})
|
||||
)
|
||||
return DummyScanner(recipe_path)
|
||||
|
||||
# Ambiguous bare name: two candidates survive (recipe base model unknown)
|
||||
scanner = write_recipe("")
|
||||
with pytest.raises(RecipeValidationError, match="include the folder path"):
|
||||
await service.reconnect_lora(
|
||||
recipe_scanner=scanner, recipe_id="r1", lora_index=0, target_name="style"
|
||||
)
|
||||
|
||||
# Confident base-model mismatch: the only candidate belongs to another family
|
||||
scanner = write_recipe("SD 1.5")
|
||||
with pytest.raises(RecipeValidationError, match="different base model"):
|
||||
await service.reconnect_lora(
|
||||
recipe_scanner=scanner,
|
||||
recipe_id="r1",
|
||||
lora_index=0,
|
||||
target_name="sdxl/style",
|
||||
)
|
||||
|
||||
# No candidate at all
|
||||
scanner = write_recipe("SDXL 1.0")
|
||||
with pytest.raises(RecipeNotFoundError, match="not found"):
|
||||
await service.reconnect_lora(
|
||||
recipe_scanner=scanner, recipe_id="r1", lora_index=0, target_name="missing"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user