mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
fix(recipes): preserve legacy gen params in modal flows
This commit is contained in:
@@ -952,6 +952,30 @@ class RecipeScanner:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Failed to update FTS index for recipe: %s", exc)
|
logger.debug("Failed to update FTS index for recipe: %s", exc)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_recipe_gen_params(recipe_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Return a recipe copy with normalized generation parameter aliases added."""
|
||||||
|
|
||||||
|
normalized_recipe = dict(recipe_data)
|
||||||
|
gen_params = recipe_data.get("gen_params")
|
||||||
|
if not isinstance(gen_params, dict):
|
||||||
|
return normalized_recipe
|
||||||
|
|
||||||
|
normalized_gen_params = dict(gen_params)
|
||||||
|
for key, value in gen_params.items():
|
||||||
|
if value in (None, ""):
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalized_key = GenParamsMerger.NORMALIZATION_MAPPING.get(key, key)
|
||||||
|
if normalized_key not in GenParamsMerger.ALLOWED_KEYS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if normalized_gen_params.get(normalized_key) in (None, ""):
|
||||||
|
normalized_gen_params[normalized_key] = value
|
||||||
|
|
||||||
|
normalized_recipe["gen_params"] = normalized_gen_params
|
||||||
|
return normalized_recipe
|
||||||
|
|
||||||
async def _enrich_cache_metadata(self) -> None:
|
async def _enrich_cache_metadata(self) -> None:
|
||||||
"""Perform remote metadata enrichment after the initial scan."""
|
"""Perform remote metadata enrichment after the initial scan."""
|
||||||
|
|
||||||
@@ -1345,6 +1369,7 @@ class RecipeScanner:
|
|||||||
# Ensure gen_params exists
|
# Ensure gen_params exists
|
||||||
if "gen_params" not in recipe_data:
|
if "gen_params" not in recipe_data:
|
||||||
recipe_data["gen_params"] = {}
|
recipe_data["gen_params"] = {}
|
||||||
|
recipe_data = self._normalize_recipe_gen_params(recipe_data)
|
||||||
|
|
||||||
# Update lora information with local paths and availability
|
# Update lora information with local paths and availability
|
||||||
lora_metadata_updated = await self._update_lora_information(recipe_data)
|
lora_metadata_updated = await self._update_lora_information(recipe_data)
|
||||||
@@ -2055,7 +2080,10 @@ class RecipeScanner:
|
|||||||
end_idx = min(start_idx + page_size, total_items)
|
end_idx = min(start_idx + page_size, total_items)
|
||||||
|
|
||||||
# Get paginated items
|
# Get paginated items
|
||||||
paginated_items = filtered_data[start_idx:end_idx]
|
paginated_items = [
|
||||||
|
self._normalize_recipe_gen_params(item)
|
||||||
|
for item in filtered_data[start_idx:end_idx]
|
||||||
|
]
|
||||||
|
|
||||||
# Add inLibrary information and URLs for each recipe
|
# Add inLibrary information and URLs for each recipe
|
||||||
for item in paginated_items:
|
for item in paginated_items:
|
||||||
@@ -2116,7 +2144,7 @@ class RecipeScanner:
|
|||||||
|
|
||||||
# Prefer the on-disk recipe JSON for fields that are not persisted in the
|
# Prefer the on-disk recipe JSON for fields that are not persisted in the
|
||||||
# SQLite cache yet, such as source_path.
|
# SQLite cache yet, such as source_path.
|
||||||
merged_recipe = {**recipe}
|
merged_recipe = self._normalize_recipe_gen_params({**recipe})
|
||||||
recipe_json = await self._load_recipe_json(recipe_id)
|
recipe_json = await self._load_recipe_json(recipe_id)
|
||||||
if recipe_json:
|
if recipe_json:
|
||||||
for field in ("source_path", "checkpoint", "loras", "gen_params"):
|
for field in ("source_path", "checkpoint", "loras", "gen_params"):
|
||||||
@@ -2181,7 +2209,7 @@ class RecipeScanner:
|
|||||||
if not isinstance(recipe_data, dict):
|
if not isinstance(recipe_data, dict):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return recipe_data
|
return self._normalize_recipe_gen_params(recipe_data)
|
||||||
|
|
||||||
def _format_file_url(self, file_path: str) -> str:
|
def _format_file_url(self, file_path: str) -> str:
|
||||||
"""Format file path as URL for serving in web UI"""
|
"""Format file path as URL for serving in web UI"""
|
||||||
|
|||||||
@@ -7,6 +7,36 @@ import { fetchRecipeDetails, updateRecipeMetadata } from '../api/recipeApi.js';
|
|||||||
import { downloadManager } from '../managers/DownloadManager.js';
|
import { downloadManager } from '../managers/DownloadManager.js';
|
||||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||||
|
|
||||||
|
const ALLOWED_GEN_PARAM_KEYS = new Set([
|
||||||
|
'prompt',
|
||||||
|
'negative_prompt',
|
||||||
|
'steps',
|
||||||
|
'sampler',
|
||||||
|
'cfg_scale',
|
||||||
|
'seed',
|
||||||
|
'size',
|
||||||
|
'clip_skip',
|
||||||
|
'denoising_strength',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const GEN_PARAM_NORMALIZATION = {
|
||||||
|
cfg: 'cfg_scale',
|
||||||
|
cfgScale: 'cfg_scale',
|
||||||
|
clipSkip: 'clip_skip',
|
||||||
|
negativePrompt: 'negative_prompt',
|
||||||
|
Sampler: 'sampler',
|
||||||
|
sampler_name: 'sampler',
|
||||||
|
scheduler: 'sampler',
|
||||||
|
Steps: 'steps',
|
||||||
|
Seed: 'seed',
|
||||||
|
Size: 'size',
|
||||||
|
Prompt: 'prompt',
|
||||||
|
'Negative prompt': 'negative_prompt',
|
||||||
|
'Cfg scale': 'cfg_scale',
|
||||||
|
'Clip skip': 'clip_skip',
|
||||||
|
'Denoising strength': 'denoising_strength',
|
||||||
|
};
|
||||||
|
|
||||||
class RecipeModal {
|
class RecipeModal {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.promptEditorState = {};
|
this.promptEditorState = {};
|
||||||
@@ -321,8 +351,13 @@ class RecipeModal {
|
|||||||
mediaContainer.appendChild(sourceUrlContainer);
|
mediaContainer.appendChild(sourceUrlContainer);
|
||||||
mediaContainer.appendChild(sourceUrlEditor);
|
mediaContainer.appendChild(sourceUrlEditor);
|
||||||
|
|
||||||
// Set up event listeners for source URL functionality
|
// Delay binding slightly so modal layout is stable, but skip if this render was torn down.
|
||||||
|
const sourceUrlContainerRef = sourceUrlContainer;
|
||||||
|
const sourceUrlEditorRef = sourceUrlEditor;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (!document.body.contains(sourceUrlContainerRef) || !document.body.contains(sourceUrlEditorRef)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setupSourceUrlHandlers();
|
this.setupSourceUrlHandlers();
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
@@ -562,18 +597,19 @@ class RecipeModal {
|
|||||||
const promptInput = document.getElementById('recipePromptInput');
|
const promptInput = document.getElementById('recipePromptInput');
|
||||||
const negativePromptInput = document.getElementById('recipeNegativePromptInput');
|
const negativePromptInput = document.getElementById('recipeNegativePromptInput');
|
||||||
const promptFieldsOnly = options.promptFieldsOnly === true;
|
const promptFieldsOnly = options.promptFieldsOnly === true;
|
||||||
|
const sanitizedGenParams = this.sanitizeGenParams(genParams);
|
||||||
|
|
||||||
if (genParams) {
|
if (sanitizedGenParams) {
|
||||||
if (!promptFieldsOnly) {
|
if (!promptFieldsOnly) {
|
||||||
this.renderPromptContent(promptElement, genParams.prompt, 'No prompt information available');
|
this.renderPromptContent(promptElement, sanitizedGenParams.prompt, 'No prompt information available');
|
||||||
this.renderPromptContent(negativePromptElement, genParams.negative_prompt, 'No negative prompt information available');
|
this.renderPromptContent(negativePromptElement, sanitizedGenParams.negative_prompt, 'No negative prompt information available');
|
||||||
|
|
||||||
if (promptInput) {
|
if (promptInput) {
|
||||||
promptInput.value = genParams.prompt || '';
|
promptInput.value = sanitizedGenParams.prompt || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (negativePromptInput) {
|
if (negativePromptInput) {
|
||||||
negativePromptInput.value = genParams.negative_prompt || '';
|
negativePromptInput.value = sanitizedGenParams.negative_prompt || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +617,7 @@ class RecipeModal {
|
|||||||
otherParamsElement.innerHTML = '';
|
otherParamsElement.innerHTML = '';
|
||||||
const excludedParams = ['prompt', 'negative_prompt'];
|
const excludedParams = ['prompt', 'negative_prompt'];
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(genParams)) {
|
for (const [key, value] of Object.entries(sanitizedGenParams)) {
|
||||||
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
||||||
const paramTag = document.createElement('div');
|
const paramTag = document.createElement('div');
|
||||||
paramTag.className = 'param-tag';
|
paramTag.className = 'param-tag';
|
||||||
@@ -612,6 +648,43 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sanitizeGenParams(genParams) {
|
||||||
|
if (!genParams || typeof genParams !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(genParams)) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ALLOWED_GEN_PARAM_KEYS.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(genParams)) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedKey = GEN_PARAM_NORMALIZATION[key] || key;
|
||||||
|
if (!ALLOWED_GEN_PARAM_KEYS.has(normalizedKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitized[normalizedKey] === undefined || sanitized[normalizedKey] === null || sanitized[normalizedKey] === '') {
|
||||||
|
sanitized[normalizedKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
syncResourcesSection(recipe = {}) {
|
syncResourcesSection(recipe = {}) {
|
||||||
const checkpointContainer = document.getElementById('recipeCheckpoint');
|
const checkpointContainer = document.getElementById('recipeCheckpoint');
|
||||||
const resourceDivider = document.getElementById('recipeResourceDivider');
|
const resourceDivider = document.getElementById('recipeResourceDivider');
|
||||||
@@ -1117,7 +1190,7 @@ class RecipeModal {
|
|||||||
|
|
||||||
const currentGenParams = this.currentRecipe.gen_params || {};
|
const currentGenParams = this.currentRecipe.gen_params || {};
|
||||||
const nextValue = input.value.trim() === '' ? '' : input.value;
|
const nextValue = input.value.trim() === '' ? '' : input.value;
|
||||||
const currentValue = currentGenParams[config.field] || '';
|
const currentValue = this.sanitizeGenParams(currentGenParams)?.[config.field] || '';
|
||||||
|
|
||||||
if (nextValue === currentValue) {
|
if (nextValue === currentValue) {
|
||||||
this.clearFieldDirty(config.field);
|
this.clearFieldDirty(config.field);
|
||||||
|
|||||||
@@ -1165,6 +1165,113 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
expect(otherParamsText).not.toContain('cfg_scale');
|
expect(otherParamsText).not.toContain('cfg_scale');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters dirty generation params from recipe modal display', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="recipeModal" class="modal">
|
||||||
|
<div id="recipeModalTitle"></div>
|
||||||
|
<div id="recipePreviewContainer"></div>
|
||||||
|
<div id="recipeTagsCompact"></div>
|
||||||
|
<div id="recipeTagsTooltip"><div id="recipeTagsTooltipContent"></div></div>
|
||||||
|
<div id="recipePrompt"></div>
|
||||||
|
<textarea id="recipePromptInput"></textarea>
|
||||||
|
<div id="recipeNegativePrompt"></div>
|
||||||
|
<textarea id="recipeNegativePromptInput"></textarea>
|
||||||
|
<div class="other-params" id="recipeOtherParams"></div>
|
||||||
|
<div id="recipeCheckpoint"></div>
|
||||||
|
<div id="recipeResourceDivider"></div>
|
||||||
|
<div id="recipeLorasList"></div>
|
||||||
|
<span id="recipeLorasCount"></span>
|
||||||
|
<button id="viewRecipeLorasBtn"></button>
|
||||||
|
<button id="copyRecipeSyntaxBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { RecipeModal } = await import('../../../static/js/components/RecipeModal.js');
|
||||||
|
const recipeModal = new RecipeModal();
|
||||||
|
|
||||||
|
recipeModal.showRecipeDetails({
|
||||||
|
id: '',
|
||||||
|
file_path: '/recipes/dirty-gen-params.json',
|
||||||
|
title: 'Dirty Gen Params Recipe',
|
||||||
|
tags: [],
|
||||||
|
file_url: '',
|
||||||
|
preview_url: '',
|
||||||
|
source_path: '',
|
||||||
|
gen_params: {
|
||||||
|
Prompt: 'visible prompt',
|
||||||
|
negativePrompt: 'visible negative',
|
||||||
|
Sampler: 'euler',
|
||||||
|
cfgScale: 7,
|
||||||
|
Version: 'ComfyUI',
|
||||||
|
raw_metadata: { prompt: 'hidden prompt' },
|
||||||
|
RNG: 'cpu',
|
||||||
|
},
|
||||||
|
loras: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherParamsText = document.getElementById('recipeOtherParams').textContent;
|
||||||
|
expect(document.getElementById('recipePrompt').textContent).toContain('visible prompt');
|
||||||
|
expect(document.getElementById('recipeNegativePrompt').textContent).toContain('visible negative');
|
||||||
|
expect(otherParamsText).toContain('sampler:');
|
||||||
|
expect(otherParamsText).toContain('cfg_scale:');
|
||||||
|
expect(otherParamsText).not.toContain('Version');
|
||||||
|
expect(otherParamsText).not.toContain('raw_metadata');
|
||||||
|
expect(otherParamsText).not.toContain('RNG');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers canonical generation params over legacy aliases in modal display', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="recipeModal" class="modal">
|
||||||
|
<div id="recipeModalTitle"></div>
|
||||||
|
<div id="recipePreviewContainer"></div>
|
||||||
|
<div id="recipeTagsCompact"></div>
|
||||||
|
<div id="recipeTagsTooltip"><div id="recipeTagsTooltipContent"></div></div>
|
||||||
|
<div id="recipePrompt"></div>
|
||||||
|
<textarea id="recipePromptInput"></textarea>
|
||||||
|
<div id="recipeNegativePrompt"></div>
|
||||||
|
<textarea id="recipeNegativePromptInput"></textarea>
|
||||||
|
<div class="other-params" id="recipeOtherParams"></div>
|
||||||
|
<div id="recipeCheckpoint"></div>
|
||||||
|
<div id="recipeResourceDivider"></div>
|
||||||
|
<div id="recipeLorasList"></div>
|
||||||
|
<span id="recipeLorasCount"></span>
|
||||||
|
<button id="viewRecipeLorasBtn"></button>
|
||||||
|
<button id="copyRecipeSyntaxBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { RecipeModal } = await import('../../../static/js/components/RecipeModal.js');
|
||||||
|
const recipeModal = new RecipeModal();
|
||||||
|
|
||||||
|
recipeModal.showRecipeDetails({
|
||||||
|
id: '',
|
||||||
|
file_path: '/recipes/canonical-wins.json',
|
||||||
|
title: 'Canonical Wins Recipe',
|
||||||
|
tags: [],
|
||||||
|
file_url: '',
|
||||||
|
preview_url: '',
|
||||||
|
source_path: '',
|
||||||
|
gen_params: {
|
||||||
|
Prompt: 'stale prompt',
|
||||||
|
prompt: 'fresh prompt',
|
||||||
|
negativePrompt: 'stale negative',
|
||||||
|
negative_prompt: 'fresh negative',
|
||||||
|
cfgScale: 3,
|
||||||
|
cfg_scale: 7,
|
||||||
|
},
|
||||||
|
loras: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherParamsText = document.getElementById('recipeOtherParams').textContent;
|
||||||
|
expect(document.getElementById('recipePrompt').textContent).toContain('fresh prompt');
|
||||||
|
expect(document.getElementById('recipePrompt').textContent).not.toContain('stale prompt');
|
||||||
|
expect(document.getElementById('recipeNegativePrompt').textContent).toContain('fresh negative');
|
||||||
|
expect(document.getElementById('recipeNegativePrompt').textContent).not.toContain('stale negative');
|
||||||
|
expect(otherParamsText).toContain('cfg_scale:');
|
||||||
|
expect(otherParamsText).toContain('7');
|
||||||
|
expect(otherParamsText).not.toContain('3');
|
||||||
|
});
|
||||||
|
|
||||||
it('replaces cached checkpoint and loras with hydrated resources', async () => {
|
it('replaces cached checkpoint and loras with hydrated resources', async () => {
|
||||||
fetchRecipeDetailsMock.mockResolvedValueOnce({
|
fetchRecipeDetailsMock.mockResolvedValueOnce({
|
||||||
id: 'recipe-resources',
|
id: 'recipe-resources',
|
||||||
@@ -1854,6 +1961,8 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
negative_prompt: 'keep negative',
|
negative_prompt: 'keep negative',
|
||||||
steps: 30,
|
steps: 30,
|
||||||
cfg_scale: 7,
|
cfg_scale: 7,
|
||||||
|
raw_metadata: { prompt: 'preserve me' },
|
||||||
|
Version: 'ComfyUI',
|
||||||
},
|
},
|
||||||
loras: [],
|
loras: [],
|
||||||
});
|
});
|
||||||
@@ -1882,6 +1991,8 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
negative_prompt: 'keep negative',
|
negative_prompt: 'keep negative',
|
||||||
steps: 30,
|
steps: 30,
|
||||||
cfg_scale: 7,
|
cfg_scale: 7,
|
||||||
|
raw_metadata: { prompt: 'preserve me' },
|
||||||
|
Version: 'ComfyUI',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ listFilePath: '/recipes/prompt.json' }
|
{ listFilePath: '/recipes/prompt.json' }
|
||||||
|
|||||||
@@ -402,6 +402,61 @@ async def test_get_recipe_by_id_merges_recipe_json_details(recipe_scanner):
|
|||||||
assert recipe["gen_params"]["prompt"] == "prompt from json"
|
assert recipe["gen_params"]["prompt"] == "prompt from json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_recipe_by_id_normalizes_gen_params_aliases_without_dropping_metadata(
|
||||||
|
recipe_scanner,
|
||||||
|
):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
recipes_dir = Path(scanner.recipes_dir)
|
||||||
|
recipe_id = "dirty-json-gen-params"
|
||||||
|
recipe_json_path = recipes_dir / f"{recipe_id}.recipe.json"
|
||||||
|
recipe_json_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/dirty-json-gen-params.png",
|
||||||
|
"title": "Dirty Recipe",
|
||||||
|
"gen_params": {
|
||||||
|
"Prompt": "prompt from json",
|
||||||
|
"negativePrompt": "negative from json",
|
||||||
|
"cfgScale": 7,
|
||||||
|
"raw_metadata": {"prompt": "nested"},
|
||||||
|
"Version": "ComfyUI",
|
||||||
|
"RNG": "cpu",
|
||||||
|
},
|
||||||
|
"loras": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
scanner._cache.raw_data = [
|
||||||
|
{
|
||||||
|
"id": recipe_id,
|
||||||
|
"file_path": "/tmp/dirty-json-gen-params.png",
|
||||||
|
"title": "Cached Recipe",
|
||||||
|
"folder": "",
|
||||||
|
"modified": 0.0,
|
||||||
|
"created_date": 0.0,
|
||||||
|
"loras": [],
|
||||||
|
"gen_params": {"prompt": "cached prompt", "raw_metadata": {"bad": True}},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
recipe = await scanner.get_recipe_by_id(recipe_id)
|
||||||
|
|
||||||
|
assert recipe is not None
|
||||||
|
assert recipe["gen_params"]["Prompt"] == "prompt from json"
|
||||||
|
assert recipe["gen_params"]["negativePrompt"] == "negative from json"
|
||||||
|
assert recipe["gen_params"]["cfgScale"] == 7
|
||||||
|
assert recipe["gen_params"]["raw_metadata"] == {"prompt": "nested"}
|
||||||
|
assert recipe["gen_params"]["Version"] == "ComfyUI"
|
||||||
|
assert recipe["gen_params"]["RNG"] == "cpu"
|
||||||
|
assert recipe["gen_params"]["prompt"] == "prompt from json"
|
||||||
|
assert recipe["gen_params"]["negative_prompt"] == "negative from json"
|
||||||
|
assert recipe["gen_params"]["cfg_scale"] == 7
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_recipe_by_id_prefers_json_file_path(recipe_scanner):
|
async def test_get_recipe_by_id_prefers_json_file_path(recipe_scanner):
|
||||||
scanner, _ = recipe_scanner
|
scanner, _ = recipe_scanner
|
||||||
@@ -528,6 +583,40 @@ async def test_get_paginated_data_filters_by_checkpoint_hash(recipe_scanner):
|
|||||||
assert [item["id"] for item in result["items"]] == ["checkpoint-match"]
|
assert [item["id"] for item in result["items"]] == ["checkpoint-match"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_paginated_data_normalizes_gen_params_aliases_without_dropping_metadata(
|
||||||
|
recipe_scanner,
|
||||||
|
):
|
||||||
|
scanner, _ = recipe_scanner
|
||||||
|
await scanner.add_recipe(
|
||||||
|
{
|
||||||
|
"id": "dirty-listing",
|
||||||
|
"file_path": str(Path(config.loras_roots[0]) / "dirty-listing.webp"),
|
||||||
|
"title": "Dirty Listing",
|
||||||
|
"modified": 0.0,
|
||||||
|
"created_date": 0.0,
|
||||||
|
"loras": [],
|
||||||
|
"gen_params": {
|
||||||
|
"Prompt": "a beautiful forest landscape",
|
||||||
|
"cfgScale": 7,
|
||||||
|
"Version": "ComfyUI",
|
||||||
|
"raw_metadata": {"bad": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
result = await scanner.get_paginated_data(page=1, page_size=10)
|
||||||
|
item = next(entry for entry in result["items"] if entry["id"] == "dirty-listing")
|
||||||
|
|
||||||
|
assert item["gen_params"]["Prompt"] == "a beautiful forest landscape"
|
||||||
|
assert item["gen_params"]["cfgScale"] == 7
|
||||||
|
assert item["gen_params"]["Version"] == "ComfyUI"
|
||||||
|
assert item["gen_params"]["raw_metadata"] == {"bad": True}
|
||||||
|
assert item["gen_params"]["prompt"] == "a beautiful forest landscape"
|
||||||
|
assert item["gen_params"]["cfg_scale"] == 7
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_recipes_for_checkpoint_matches_hash_case_insensitively(recipe_scanner):
|
async def test_get_recipes_for_checkpoint_matches_hash_case_insensitively(recipe_scanner):
|
||||||
scanner, _ = recipe_scanner
|
scanner, _ = recipe_scanner
|
||||||
|
|||||||
Reference in New Issue
Block a user