feat: Introduce recipe management with data models, scanning, enrichment, and repair for generation configurations.

This commit is contained in:
Will Miao
2025-12-24 20:02:20 +08:00
parent 6330c65d41
commit 6486107ca2
8 changed files with 421 additions and 157 deletions

View File

@@ -338,7 +338,7 @@ async def test_move_recipe_invokes_persistence(monkeypatch, tmp_path: Path) -> N
async def test_import_remote_recipe(monkeypatch, tmp_path: Path) -> None:
provider_calls: list[int] = []
provider_calls: list[str | int] = []
class Provider:
async def get_model_version_info(self, model_version_id):
@@ -348,7 +348,7 @@ async def test_import_remote_recipe(monkeypatch, tmp_path: Path) -> None:
async def fake_get_default_metadata_provider():
return Provider()
monkeypatch.setattr(recipe_handlers, "get_default_metadata_provider", fake_get_default_metadata_provider)
monkeypatch.setattr("py.recipes.enrichment.get_default_metadata_provider", fake_get_default_metadata_provider)
async with recipe_harness(monkeypatch, tmp_path) as harness:
resources = [
@@ -390,7 +390,7 @@ async def test_import_remote_recipe(monkeypatch, tmp_path: Path) -> None:
assert call["tags"] == ["foo", "bar"]
metadata = call["metadata"]
assert metadata["base_model"] == "Flux Provider"
assert provider_calls == [33]
assert provider_calls == ["33"]
assert metadata["checkpoint"]["modelVersionId"] == 33
assert metadata["loras"][0]["weight"] == 0.25
assert metadata["gen_params"]["prompt"] == "hello world"
@@ -399,7 +399,7 @@ async def test_import_remote_recipe(monkeypatch, tmp_path: Path) -> None:
async def test_import_remote_recipe_falls_back_to_request_base_model(monkeypatch, tmp_path: Path) -> None:
provider_calls: list[int] = []
provider_calls: list[str | int] = []
class Provider:
async def get_model_version_info(self, model_version_id):
@@ -409,7 +409,7 @@ async def test_import_remote_recipe_falls_back_to_request_base_model(monkeypatch
async def fake_get_default_metadata_provider():
return Provider()
monkeypatch.setattr(recipe_handlers, "get_default_metadata_provider", fake_get_default_metadata_provider)
monkeypatch.setattr("py.recipes.enrichment.get_default_metadata_provider", fake_get_default_metadata_provider)
async with recipe_harness(monkeypatch, tmp_path) as harness:
resources = [
@@ -438,14 +438,14 @@ async def test_import_remote_recipe_falls_back_to_request_base_model(monkeypatch
metadata = harness.persistence.save_calls[-1]["metadata"]
assert metadata["base_model"] == "Flux"
assert provider_calls == [77]
assert provider_calls == ["77"]
async def test_import_remote_video_recipe(monkeypatch, tmp_path: Path) -> None:
async def fake_get_default_metadata_provider():
return SimpleNamespace(get_model_version_info=lambda id: ({}, None))
monkeypatch.setattr(recipe_handlers, "get_default_metadata_provider", fake_get_default_metadata_provider)
monkeypatch.setattr("py.recipes.enrichment.get_default_metadata_provider", fake_get_default_metadata_provider)
async with recipe_harness(monkeypatch, tmp_path) as harness:
harness.civitai.image_info["12345"] = {
@@ -537,7 +537,7 @@ async def test_import_remote_recipe_merges_metadata(monkeypatch, tmp_path: Path)
async def fake_get_default_metadata_provider():
return Provider()
monkeypatch.setattr(recipe_handlers, "get_default_metadata_provider", fake_get_default_metadata_provider)
monkeypatch.setattr("py.recipes.enrichment.get_default_metadata_provider", fake_get_default_metadata_provider)
# 2. Mock ExifUtils to return some embedded metadata
class MockExifUtils:

View File

@@ -57,3 +57,38 @@ def test_merge_filters_blacklisted_keys():
assert "id" not in merged
assert "url" not in merged
assert "hash" not in merged
def test_merge_filters_meta_and_normalizes_keys():
civitai_meta = {
"prompt": "masterpiece",
"cfgScale": 5,
"clipSkip": 2,
"negativePrompt": "low quality",
"meta": {"irrelevant": "data"},
"Size": "1024x1024",
"draft": False,
"workflow": "txt2img",
"civitaiResources": [{"type": "checkpoint"}]
}
request_params = {
"cfg_scale": 5.0,
"clip_skip": "2",
"Steps": 30
}
merged = GenParamsMerger.merge(request_params, civitai_meta)
assert "meta" not in merged
assert "cfgScale" not in merged
assert "clipSkip" not in merged
assert "negativePrompt" not in merged
assert "Size" not in merged
assert "draft" not in merged
assert "workflow" not in merged
assert "civitaiResources" not in merged
assert merged["cfg_scale"] == 5.0 # From request_params
assert merged["clip_skip"] == "2" # From request_params
assert merged["negative_prompt"] == "low quality" # Normalized from civitai_meta
assert merged["size"] == "1024x1024" # Normalized from civitai_meta
assert merged["steps"] == 30 # Normalized from request_params

View File

@@ -43,7 +43,7 @@ def setup_scanner(recipe_scanner, mock_civitai_client, mock_metadata_provider, m
mock_save = AsyncMock(side_effect=real_save)
monkeypatch.setattr(recipe_scanner, "_save_recipe_persistently", mock_save)
monkeypatch.setattr("py.services.recipe_scanner.get_default_metadata_provider", AsyncMock(return_value=mock_metadata_provider))
monkeypatch.setattr("py.recipes.enrichment.get_default_metadata_provider", AsyncMock(return_value=mock_metadata_provider))
# Mock get_recipe_json_path to avoid file system issues in tests
recipe_scanner.get_recipe_json_path = AsyncMock(return_value="/tmp/test_recipe.json")
@@ -259,11 +259,7 @@ async def test_repair_all_recipes_strips_runtime_fields(setup_scanner):
@pytest.mark.asyncio
async def test_sanitize_recipe_for_storage(recipe_scanner):
import sys
import py.services.recipe_scanner
print(f"\nDEBUG_ENV: sys.path: {sys.path}")
print(f"DEBUG_ENV: recipe_scanner file: {py.services.recipe_scanner.__file__}")
recipe = {
"loras": [{"name": "L1", "inLibrary": True, "weight": 0.5}],
"checkpoint": {"name": "CP", "localPath": "/tmp/cp"}