Treat model_gguf as diffusion_models and render accepted file formats in missing-model UI

This commit is contained in:
Dariusz L
2026-07-20 16:54:28 +02:00
parent 097be623f4
commit d293b0aaf8
9 changed files with 113 additions and 11 deletions
+7 -1
View File
@@ -73,7 +73,13 @@ TEMPLATE_KEY_ALIASES = {
"loras": ("loras", "lora"),
"checkpoints": ("checkpoints", "checkpoint"),
"embeddings": ("embeddings", "embedding"),
"diffusion_models": ("diffusion_models", "diffusion_model", "unet", "unet_gguf"),
"diffusion_models": (
"diffusion_models",
"diffusion_model",
"unet",
"unet_gguf",
"model_gguf",
),
"text_encoders": ("text_encoders", "text_encoder", "clip", "clips", "clip_gguf"),
"controlnet": ("controlnet", "control_net"),
"vae": ("vae",),
+2
View File
@@ -253,6 +253,7 @@ CATEGORY_MAP = {
"diffusers": "diffusers",
"unet": "diffusion_models",
"unet_gguf": "diffusion_models",
"model_gguf": "diffusion_models",
"select_safetensors": "diffusion_models",
"diffusion_model": "diffusion_models",
"diffusion_models": "diffusion_models",
@@ -1130,6 +1131,7 @@ def get_category_folder_keys(category: str) -> list[str]:
if folder_key == "diffusion_models":
keys.append("unet")
keys.append("unet_gguf")
keys.append("model_gguf")
elif folder_key == "text_encoders":
keys.append("clip")
return keys
+7 -2
View File
@@ -22,7 +22,12 @@ except ImportError:
log.warning("Model Resolver: folder_paths not available yet - will retry later")
from .type_utils import MODEL_EXTENSIONS, URN_TYPE_MAP, unique_ordered_strings
from .type_utils import (
MODEL_EXTENSIONS,
URN_TYPE_MAP,
normalize_download_category,
unique_ordered_strings,
)
URN_REGEX = re.compile(r"^urn:air:([^:]+):([^:]+):([^:]+):(\d+)@(\d+)$")
@@ -1024,7 +1029,7 @@ def get_model_widget_category_hints(
hints_list = []
for h in dynamic_category_hints:
if h:
hints_list.append(h)
hints_list.append(normalize_download_category(h))
if dynamic_category_hints:
return _ordered_unique_categories(hints_list)
+30 -4
View File
@@ -34,6 +34,10 @@ const modelInfoMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/views/model_info_methods.js'),
'utf8'
);
const missingBrowserMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/views/missing_browser_methods.js'),
'utf8'
);
const renderFormatMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/utils/render_format_methods.js'),
'utf8'
@@ -1827,7 +1831,7 @@ test('local match bad folder badge is omitted for supported folder', () => {
assert.equal(html, '');
});
test('local match bad folder badge treats unet gguf as diffusion models', () => {
test('local match bad folder badge treats gguf folder keys as diffusion models', () => {
const getLocalMatchCategory = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchCategory')})`);
const getLocalMatchBadFolderWarning = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchBadFolderWarning')})`);
const renderLocalMatchBadFolderBadge = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchBadFolderBadge')})`);
@@ -1837,7 +1841,7 @@ test('local match bad folder badge treats unet gguf as diffusion models', () =>
normalizeDownloadCategory(value = '') {
return {
unet_gguf: 'diffusion_models',
diffusion_models: 'diffusion_models',
model_gguf: 'diffusion_models',
}[String(value || '').trim()] || String(value || '').trim();
},
getMissingSupportedDownloadCategories() {
@@ -1851,7 +1855,7 @@ test('local match bad folder badge treats unet gguf as diffusion models', () =>
const html = renderLocalMatchBadFolderBadge.call(
dialog,
{ category: 'diffusion_models' },
{ confidence: 100, model: { category: 'unet_gguf' } }
{ confidence: 100, model: { category: 'model_gguf' } }
);
assert.equal(html, '');
@@ -1925,11 +1929,12 @@ test('local match bad type badge is omitted for a file type accepted by node cat
assert.equal(html, '');
});
test('download category normalization maps unet gguf to diffusion models', () => {
test('download category normalization maps gguf folder keys to diffusion models', () => {
const normalizeDownloadCategory = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeDownloadCategory')})`);
assert.equal(normalizeDownloadCategory('unet_gguf'), 'diffusion_models');
assert.equal(normalizeDownloadCategory('UNET GGUF'), 'diffusion_models');
assert.equal(normalizeDownloadCategory('model_gguf'), 'diffusion_models');
});
test('download category normalization maps select safetensors alias to diffusion models', () => {
@@ -1939,6 +1944,27 @@ test('download category normalization maps select safetensors alias to diffusion
assert.equal(normalizeDownloadCategory('SELECT SAFETENSORS'), 'diffusion_models');
});
test('missing model preview shows accepted GGUF format next to its category', () => {
const renderMissingModelFormatBadges = eval(`(${extractMethod(missingBrowserMethodsSource, 'renderMissingModelFormatBadges')})`);
const dialog = {
getMissingAcceptedModelFileTypes() {
return [{ extension: 'gguf', label: 'GGUF', display: 'GGUF (.gguf)' }];
},
escapeHtml(value) {
return String(value).replace(/[&<>\"]/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[char]));
},
};
const html = renderMissingModelFormatBadges.call(dialog, {
category: 'diffusion_models',
node_type: 'UnetLoaderGGUF',
});
assert.match(html, /mr-model-format-chip/);
assert.match(html, />GGUF<\/span>/);
assert.match(html, /Accepted file format for this node: GGUF \(\.gguf\)\./);
});
test('hash match label map numbers distinct matched hashes', () => {
const normalizeSearchResultSha256 = eval(`(${extractMethod(searchPanelMethodsSource, 'normalizeSearchResultSha256')})`);
const getLocalMatchHash = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchHash')})`);
+9 -4
View File
@@ -246,8 +246,15 @@ class TestRefactoringTargets(unittest.IsolatedAsyncioTestCase):
def test_category_folder_keys_mapping(self):
from core.type_utils import get_category_folder_keys
self.assertEqual(get_category_folder_keys("diffusion_models"), ["diffusion_models", "unet", "unet_gguf"])
self.assertEqual(get_category_folder_keys("unet_gguf"), ["diffusion_models", "unet", "unet_gguf"])
expected_diffusion_keys = [
"diffusion_models",
"unet",
"unet_gguf",
"model_gguf",
]
self.assertEqual(get_category_folder_keys("diffusion_models"), expected_diffusion_keys)
self.assertEqual(get_category_folder_keys("model_gguf"), expected_diffusion_keys)
self.assertEqual(get_category_folder_keys("unet_gguf"), expected_diffusion_keys)
self.assertEqual(get_category_folder_keys("text_encoders"), ["text_encoders", "clip"])
self.assertEqual(get_category_folder_keys("checkpoints"), ["checkpoints"])
@@ -298,5 +305,3 @@ class TestRefactoringTargets(unittest.IsolatedAsyncioTestCase):
# Require download excludes main.safetensors because it has no download url
self.assertEqual(select_primary_model_file(files, require_download=True)["id"], 1)
+35
View File
@@ -1,6 +1,7 @@
import os
import tempfile
import unittest
from unittest.mock import patch
from core.scanner import scan_directory
from core.workflow_analyzer import analyze_workflow_models, identify_missing_models
@@ -412,6 +413,40 @@ class WorkflowAnalyzerCategoryHintTests(unittest.TestCase):
self.assertEqual(4, len(refs))
self.assertEqual({"text_encoders"}, {ref["category"] for ref in refs})
def test_gguf_unet_loader_keeps_diffusion_badge_and_raw_folder_hint(self):
for node_type in (
"LoaderGGUF",
"LoaderGGUFAdvanced",
"UnetLoaderGGUF",
"UnetLoaderGGUFAdvanced",
):
with self.subTest(node_type=node_type):
with patch(
"core.workflow_analyzer.get_dynamic_widget_category_hints",
return_value=["model_gguf"],
):
refs = analyze_workflow_models(
{
"nodes": [
{
"id": 345,
"type": node_type,
"widgets": [{"name": "gguf_name"}],
"widgets_values": [
r"LTXV\LTX-2.3-22B-distilled-1.1-Q4_K_M.gguf"
],
"outputs": [{"type": "MODEL", "links": [1]}],
}
]
},
available_models=[],
)
self.assertEqual(1, len(refs))
self.assertEqual("diffusion_models", refs[0]["category"])
self.assertEqual(["diffusion_models"], refs[0]["category_hints"])
self.assertEqual(["model_gguf"], refs[0]["folder_key_hints"])
class ScannerFolderModelTests(unittest.TestCase):
def test_diffusers_folder_models_are_scanned_as_folder_entries(self):
+6
View File
@@ -1489,6 +1489,12 @@
font-weight: 700;
}
.mr-model-format-chip {
background: rgba(67, 160, 255, 0.12);
border-color: rgba(104, 184, 255, 0.3);
color: #b9ddff;
}
/* ComfyUI node_slot-inspired type chips. */
.mr-type-chip,
.mr-info-tag.mr-type-chip,
@@ -109,6 +109,7 @@ export const downloadTargetMethods = {
upscaler: 'upscale_models',
unet: 'diffusion_models',
unet_gguf: 'diffusion_models',
model_gguf: 'diffusion_models',
select_safetensors: 'diffusion_models',
diffusion_model: 'diffusion_models',
diffusion_models: 'diffusion_models',
@@ -297,6 +298,10 @@ export const downloadTargetMethods = {
CheckpointLoader: 'checkpoints',
UNETLoader: 'diffusion_models',
UNETLoaderAdvanced: 'diffusion_models',
LoaderGGUF: 'diffusion_models',
LoaderGGUFAdvanced: 'diffusion_models',
UnetLoaderGGUF: 'diffusion_models',
UnetLoaderGGUFAdvanced: 'diffusion_models',
VAELoader: 'vae',
VAELoaderKJ: 'vae',
LoraLoader: 'loras',
@@ -12,6 +12,17 @@ export const missingBrowserMethods = {
return this.getFilenameFromPath(missing.original_path) || missing.name || 'Missing model';
},
renderMissingModelFormatBadges(missing = {}) {
const acceptedTypes = this.getMissingAcceptedModelFileTypes?.(missing) || [];
return acceptedTypes.map(type => {
const label = String(type?.label || type?.extension || '').trim();
if (!label) return '';
const display = String(type?.display || label).trim();
const tooltip = `Accepted file format for this node: ${display}.`;
return `<span class="mr-category-chip mr-model-format-chip" data-tooltip="${this.escapeHtml(tooltip)}">${this.escapeHtml(label)}</span>`;
}).join('');
},
getMissingLocateTarget(missing = {}) {
const hasLocateNode = missing.locate_node_id !== undefined
&& missing.locate_node_id !== null
@@ -2079,6 +2090,7 @@ export const missingBrowserMethods = {
if (missing.category) {
html += `<span class="mr-category-chip ${this.getModelTypeColorClass(missing.category)}" data-tooltip="${this.escapeHtml(missing.category)}">${this.getCategoryDisplayName(missing.category)}</span>`;
}
html += this.renderMissingModelFormatBadges(missing);
html += `<span id="${locateId}" class="${nodeChipClasses}"${nodeChipTitle ? ` data-tooltip="${this.escapeHtml(nodeChipTitle)}"` : ''}>`;
if (nodeDisplay.canLocate) {
html += this.getLocateIconHtml();