mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-22 11:21:15 -03:00
Compare commits
2 Commits
31c54ff068
...
a74cbe7aa2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a74cbe7aa2 | ||
|
|
94edfaa190 |
@@ -871,28 +871,47 @@ class RecipeManagementHandler:
|
|||||||
"Failed to extract embedded metadata during import: %s", exc
|
"Failed to extract embedded metadata during import: %s", exc
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fallback: if EXIF extraction yielded nothing, parse Civitai API meta directly
|
# Parse CivitAI API meta to discover all resources from modelVersionIds
|
||||||
# (same approach as analyze_remote_image — downloaded Civitai images often
|
# (modelVersionIds is injected at root level by _download_remote_media).
|
||||||
# have no embedded EXIF but the API meta contains resources/hashes)
|
# Run unconditionally — EXIF parsing may succeed for gen_params but miss
|
||||||
if parsed_embedded is None and civitai_meta_raw:
|
# LoRAs since modelVersionIds is NOT embedded in the image EXIF.
|
||||||
|
civitai_parsed = None
|
||||||
|
if civitai_meta_raw:
|
||||||
civitai_inner_meta = civitai_meta_raw
|
civitai_inner_meta = civitai_meta_raw
|
||||||
if isinstance(civitai_meta_raw, dict) and "meta" in civitai_meta_raw:
|
if isinstance(civitai_meta_raw, dict) and "meta" in civitai_meta_raw:
|
||||||
civitai_inner_meta = civitai_meta_raw["meta"]
|
civitai_inner_meta = civitai_meta_raw["meta"]
|
||||||
|
# modelVersionIds lives at outer meta level; propagate after unwrap
|
||||||
|
_mvids = civitai_meta_raw.get("modelVersionIds")
|
||||||
|
if _mvids and isinstance(civitai_inner_meta, dict):
|
||||||
|
civitai_inner_meta["modelVersionIds"] = _mvids
|
||||||
if isinstance(civitai_inner_meta, dict):
|
if isinstance(civitai_inner_meta, dict):
|
||||||
parser = self._analysis_service._recipe_parser_factory.create_parser(
|
parser = self._analysis_service._recipe_parser_factory.create_parser(
|
||||||
civitai_inner_meta
|
civitai_inner_meta
|
||||||
)
|
)
|
||||||
if parser:
|
if parser:
|
||||||
parsed_embedded = await parser.parse_metadata(
|
civitai_parsed = await parser.parse_metadata(
|
||||||
civitai_inner_meta, recipe_scanner=recipe_scanner
|
civitai_inner_meta, recipe_scanner=recipe_scanner
|
||||||
)
|
)
|
||||||
if parsed_embedded and "gen_params" in parsed_embedded:
|
if civitai_parsed and "gen_params" in civitai_parsed:
|
||||||
embedded_gen_params = parsed_embedded["gen_params"]
|
# Merge: API gen_params override EXIF at field level,
|
||||||
|
# EXIF fills in fields the API doesn't have.
|
||||||
|
embedded_gen_params = {
|
||||||
|
**(embedded_gen_params or {}),
|
||||||
|
**civitai_parsed["gen_params"],
|
||||||
|
}
|
||||||
|
|
||||||
if embedded_gen_params:
|
if embedded_gen_params:
|
||||||
metadata["gen_params"] = embedded_gen_params
|
metadata["gen_params"] = embedded_gen_params
|
||||||
|
|
||||||
if parsed_embedded:
|
# Merge LoRAs: prefer frontend resources, supplement with CivitAI modelVersionIds
|
||||||
|
if civitai_parsed:
|
||||||
|
civitai_loras = civitai_parsed.get("loras", [])
|
||||||
|
if civitai_loras and not metadata.get("loras"):
|
||||||
|
metadata["loras"] = civitai_loras
|
||||||
|
civitai_model = civitai_parsed.get("model")
|
||||||
|
if civitai_model and not metadata.get("checkpoint"):
|
||||||
|
metadata["checkpoint"] = civitai_model
|
||||||
|
elif parsed_embedded:
|
||||||
parsed_loras = parsed_embedded.get("loras")
|
parsed_loras = parsed_embedded.get("loras")
|
||||||
if parsed_loras and not metadata.get("loras"):
|
if parsed_loras and not metadata.get("loras"):
|
||||||
metadata["loras"] = parsed_loras
|
metadata["loras"] = parsed_loras
|
||||||
@@ -1270,16 +1289,29 @@ class RecipeManagementHandler:
|
|||||||
|
|
||||||
with open(temp_path, "rb") as file_obj:
|
with open(temp_path, "rb") as file_obj:
|
||||||
model_ver_id = None
|
model_ver_id = None
|
||||||
|
civitai_meta_raw = (
|
||||||
|
image_info.get("meta") if civitai_image_id and image_info else None
|
||||||
|
)
|
||||||
if civitai_image_id and image_info:
|
if civitai_image_id and image_info:
|
||||||
model_ver_id = image_info.get("modelVersionId")
|
model_ver_id = image_info.get("modelVersionId")
|
||||||
if not model_ver_id:
|
if not model_ver_id:
|
||||||
ids = image_info.get("modelVersionIds")
|
ids = image_info.get("modelVersionIds")
|
||||||
if isinstance(ids, list) and ids:
|
if isinstance(ids, list) and ids:
|
||||||
model_ver_id = ids[0]
|
model_ver_id = ids[0]
|
||||||
|
|
||||||
|
# Inject root-level modelVersionIds into meta so downstream
|
||||||
|
# parsers (CivitaiApiMetadataParser) can discover ALL resources
|
||||||
|
# (checkpoint + LoRAs), not just the first model version ID.
|
||||||
|
# CivitAI API returns modelVersionIds at the root level of
|
||||||
|
# the image response, NOT inside the meta object.
|
||||||
|
mvids = image_info.get("modelVersionIds")
|
||||||
|
if mvids and isinstance(civitai_meta_raw, dict):
|
||||||
|
civitai_meta_raw["modelVersionIds"] = mvids
|
||||||
|
|
||||||
return (
|
return (
|
||||||
file_obj.read(),
|
file_obj.read(),
|
||||||
extension,
|
extension,
|
||||||
image_info.get("meta") if civitai_image_id and image_info else None,
|
civitai_meta_raw,
|
||||||
model_ver_id,
|
model_ver_id,
|
||||||
)
|
)
|
||||||
except RecipeDownloadError:
|
except RecipeDownloadError:
|
||||||
@@ -1467,20 +1499,34 @@ class RecipeManagementHandler:
|
|||||||
"Failed to extract embedded metadata: %s", exc
|
"Failed to extract embedded metadata: %s", exc
|
||||||
)
|
)
|
||||||
|
|
||||||
if parsed_embedded is None and civitai_meta_raw:
|
# Parse CivitAI API meta to discover all resources from modelVersionIds.
|
||||||
|
# Run unconditionally — EXIF parsing succeeds for gen_params but misses
|
||||||
|
# LoRAs (modelVersionIds is NOT in the image EXIF).
|
||||||
|
civitai_parsed = None
|
||||||
|
if civitai_meta_raw:
|
||||||
civitai_inner_meta = civitai_meta_raw
|
civitai_inner_meta = civitai_meta_raw
|
||||||
if isinstance(civitai_meta_raw, dict) and "meta" in civitai_meta_raw:
|
if isinstance(civitai_meta_raw, dict) and "meta" in civitai_meta_raw:
|
||||||
civitai_inner_meta = civitai_meta_raw["meta"]
|
civitai_inner_meta = civitai_meta_raw["meta"]
|
||||||
|
# Propagate modelVersionIds into unwrapped meta — it lives
|
||||||
|
# at the outer meta level in the CivitAI API response.
|
||||||
|
_mvids = civitai_meta_raw.get("modelVersionIds")
|
||||||
|
if _mvids and isinstance(civitai_inner_meta, dict):
|
||||||
|
civitai_inner_meta["modelVersionIds"] = _mvids
|
||||||
if isinstance(civitai_inner_meta, dict):
|
if isinstance(civitai_inner_meta, dict):
|
||||||
parser = self._analysis_service._recipe_parser_factory.create_parser(
|
parser = self._analysis_service._recipe_parser_factory.create_parser(
|
||||||
civitai_inner_meta
|
civitai_inner_meta
|
||||||
)
|
)
|
||||||
if parser:
|
if parser:
|
||||||
parsed_embedded = await parser.parse_metadata(
|
civitai_parsed = await parser.parse_metadata(
|
||||||
civitai_inner_meta, recipe_scanner=recipe_scanner
|
civitai_inner_meta, recipe_scanner=recipe_scanner
|
||||||
)
|
)
|
||||||
if parsed_embedded and "gen_params" in parsed_embedded:
|
if civitai_parsed and "gen_params" in civitai_parsed:
|
||||||
embedded_gen_params = parsed_embedded["gen_params"]
|
# Merge: API gen_params override EXIF at field level,
|
||||||
|
# EXIF fills in fields the API doesn't have.
|
||||||
|
embedded_gen_params = {
|
||||||
|
**(embedded_gen_params or {}),
|
||||||
|
**civitai_parsed["gen_params"],
|
||||||
|
}
|
||||||
|
|
||||||
metadata: Dict[str, Any] = {
|
metadata: Dict[str, Any] = {
|
||||||
"base_model": "",
|
"base_model": "",
|
||||||
@@ -1489,7 +1535,14 @@ class RecipeManagementHandler:
|
|||||||
"source_path": image_url,
|
"source_path": image_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed_embedded:
|
if civitai_parsed:
|
||||||
|
civitai_loras = civitai_parsed.get("loras", [])
|
||||||
|
if civitai_loras and not metadata.get("loras"):
|
||||||
|
metadata["loras"] = civitai_loras
|
||||||
|
civitai_model = civitai_parsed.get("model")
|
||||||
|
if civitai_model and not metadata.get("checkpoint"):
|
||||||
|
metadata["checkpoint"] = civitai_model
|
||||||
|
elif parsed_embedded:
|
||||||
parsed_loras = parsed_embedded.get("loras")
|
parsed_loras = parsed_embedded.get("loras")
|
||||||
if parsed_loras and not metadata.get("loras"):
|
if parsed_loras and not metadata.get("loras"):
|
||||||
metadata["loras"] = parsed_loras
|
metadata["loras"] = parsed_loras
|
||||||
|
|||||||
@@ -785,10 +785,16 @@ async def test_import_remote_recipe_merges_metadata(
|
|||||||
async def parse_metadata(self, raw, recipe_scanner=None):
|
async def parse_metadata(self, raw, recipe_scanner=None):
|
||||||
return json.loads(raw[len("Recipe metadata: ") :])
|
return json.loads(raw[len("Recipe metadata: ") :])
|
||||||
|
|
||||||
|
class MockApiParser:
|
||||||
|
async def parse_metadata(self, raw, recipe_scanner=None):
|
||||||
|
return {"gen_params": raw, "loras": []}
|
||||||
|
|
||||||
class MockFactory:
|
class MockFactory:
|
||||||
def create_parser(self, raw):
|
def create_parser(self, raw):
|
||||||
if raw.startswith("Recipe metadata: "):
|
if isinstance(raw, str) and raw.startswith("Recipe metadata: "):
|
||||||
return MockParser()
|
return MockParser()
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
return MockApiParser()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 4. Setup Harness and run test
|
# 4. Setup Harness and run test
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ async def test_get_model_versions_raises_on_other_errors(monkeypatch, downloader
|
|||||||
async def test_get_model_versions_bulk_success(monkeypatch, downloader):
|
async def test_get_model_versions_bulk_success(monkeypatch, downloader):
|
||||||
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
||||||
assert url.endswith("/models")
|
assert url.endswith("/models")
|
||||||
assert kwargs.get("params") == {"ids": "1,2"}
|
assert kwargs.get("params") == {"ids": "1,2", "nsfw": "true"}
|
||||||
return True, {
|
return True, {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user