mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
Compare commits
3 Commits
a7995db009
...
e747946f7a
| Author | SHA1 | Date | |
|---|---|---|---|
| e747946f7a | |||
| 53fa22f39c | |||
| 82b34097fb |
@@ -122,12 +122,8 @@ async def _save_hf_metadata(dest_path: str, repo: str, model_root: str) -> None:
|
||||
metadata._unknown_fields["hf_url"] = hf_url
|
||||
metadata.from_civitai = False # HF models are not from CivitAI
|
||||
|
||||
metadata_dict = metadata.to_dict()
|
||||
if "trainedWords" in metadata_dict and not metadata_dict["trainedWords"]:
|
||||
del metadata_dict["trainedWords"]
|
||||
|
||||
# 3. Save metadata atomically
|
||||
await MetadataManager.save_metadata(dest_path, metadata_dict)
|
||||
await MetadataManager.save_metadata(dest_path, metadata)
|
||||
logger.info("Saved HF metadata (with hf_url) for %s", dest_path)
|
||||
|
||||
# 4. Determine relative folder path for cache
|
||||
|
||||
@@ -407,7 +407,6 @@ class AgentService:
|
||||
"base_model": metadata.get("base_model", ""),
|
||||
"tags": metadata.get("tags", []),
|
||||
"modelDescription": metadata.get("modelDescription", ""),
|
||||
"trainedWords": metadata.get("trainedWords", []),
|
||||
"sha256": (metadata.get("sha256") or "")[:16] + "..." if metadata.get("sha256") else "",
|
||||
"size": metadata.get("size", 0),
|
||||
}
|
||||
|
||||
@@ -77,9 +77,6 @@ class BaseModelMetadata:
|
||||
last_checked_at: float = 0 # Last checked timestamp
|
||||
hash_status: str = "completed" # Hash calculation status: pending | calculating | completed | failed
|
||||
autov3: Optional[str] = None # CivitAI AutoV3 hash (12-char lowercase hex); "" = checked but unavailable, None = not checked
|
||||
trainedWords: List[str] = field(
|
||||
default_factory=list
|
||||
) # Trigger words / activation prompts (source-agnostic)
|
||||
_unknown_fields: Dict[str, Any] = field(
|
||||
default_factory=dict, repr=False, compare=False
|
||||
) # Store unknown fields
|
||||
@@ -92,9 +89,6 @@ class BaseModelMetadata:
|
||||
if self.tags is None:
|
||||
self.tags = []
|
||||
|
||||
if self.trainedWords is None:
|
||||
self.trainedWords = []
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "BaseModelMetadata":
|
||||
"""Create instance from dictionary"""
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Keep the fixed-position sidebar anchored when highlighted, otherwise
|
||||
.onboarding-target-highlight's position: relative would pull it into
|
||||
normal flow and it would move away from the spotlight cutout */
|
||||
.folder-sidebar.onboarding-target-highlight {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.onboarding-popup {
|
||||
position: absolute;
|
||||
background: var(--lora-surface);
|
||||
|
||||
@@ -100,7 +100,7 @@ def evaluate_model(
|
||||
flagged issues.
|
||||
"""
|
||||
civitai = metadata.get("civitai") or {}
|
||||
trained_words: List[str] = civitai.get("trainedWords") or metadata.get("trainedWords") or []
|
||||
trained_words: List[str] = civitai.get("trainedWords") or []
|
||||
short_desc: str = civitai.get("description") or ""
|
||||
tags: List[str] = metadata.get("tags") or []
|
||||
notes: str = metadata.get("notes") or ""
|
||||
|
||||
@@ -149,7 +149,6 @@ def create_initial_metadata(
|
||||
"metadata_source": "",
|
||||
"last_checked_at": 0,
|
||||
"hash_status": "completed",
|
||||
"trainedWords": [],
|
||||
"hf_url": hf_url,
|
||||
"usage_tips": "{}",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
const {
|
||||
API_MODULE,
|
||||
APP_MODULE,
|
||||
AUTOCOMPLETE_MODULE,
|
||||
} = vi.hoisted(() => ({
|
||||
API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname,
|
||||
APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname,
|
||||
AUTOCOMPLETE_MODULE: new URL('../../../web/comfyui/autocomplete.js', import.meta.url).pathname,
|
||||
}));
|
||||
|
||||
vi.mock(API_MODULE, () => ({
|
||||
api: {
|
||||
fetchApi: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock(APP_MODULE, () => ({
|
||||
app: {
|
||||
canvas: {
|
||||
ds: { scale: 1 },
|
||||
},
|
||||
extensionManager: {
|
||||
setting: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
},
|
||||
registerExtension: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('formatAutocompleteTextOnBlur', () => {
|
||||
it('preserves repeated spaces inside LoRA names', async () => {
|
||||
const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE);
|
||||
|
||||
expect(formatAutocompleteTextOnBlur('<lora:test - 0021:1.00>')).toBe(
|
||||
'<lora:test - 0021:1.00>'
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves repeated spaces across multiple LoRA entries', async () => {
|
||||
const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE);
|
||||
|
||||
expect(
|
||||
formatAutocompleteTextOnBlur('<lora:test - 0021:1.00>,<lora:a b:0.50>')
|
||||
).toBe('<lora:test - 0021:1.00>, <lora:a b:0.50>');
|
||||
});
|
||||
|
||||
it('still normalizes whitespace outside LoRA tags', async () => {
|
||||
const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE);
|
||||
|
||||
expect(formatAutocompleteTextOnBlur('masterpiece, best quality')).toBe(
|
||||
'masterpiece, best quality'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,15 @@ describe("applyLoraValuesToText", () => {
|
||||
|
||||
expect(result).toBe("<lora:Expanded:1.00:1.00>");
|
||||
});
|
||||
|
||||
it("preserves repeated spaces inside LoRA names", () => {
|
||||
const original = "<lora:test - 0021:1.00>";
|
||||
const result = applyLoraValuesToText(original, [
|
||||
{ name: "test - 0021", strength: 0.5 }
|
||||
]);
|
||||
|
||||
expect(result).toBe("<lora:test - 0021:0.50>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeStrengthValue", () => {
|
||||
@@ -74,6 +83,18 @@ describe("cleanupLoraSyntax", () => {
|
||||
it("collapses whitespace and stray commas", () => {
|
||||
expect(cleanupLoraSyntax(" <lora:A:1.00> , ," )).toBe("<lora:A:1.00>");
|
||||
});
|
||||
|
||||
it("preserves repeated spaces inside LoRA names", () => {
|
||||
expect(cleanupLoraSyntax("<lora:test - 0021:1.00> , ,")).toBe(
|
||||
"<lora:test - 0021:1.00>"
|
||||
);
|
||||
});
|
||||
|
||||
it("still normalizes whitespace between entries", () => {
|
||||
expect(
|
||||
cleanupLoraSyntax(" <lora:A:1.00> <lora:test - 0021:0.50> ")
|
||||
).toBe("<lora:A:1.00> <lora:test - 0021:0.50>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("debounce", () => {
|
||||
|
||||
@@ -164,7 +164,7 @@ class TestEnrichHfMetadata:
|
||||
skill_name="enrich_hf_metadata",
|
||||
model_path="/p.safetensors",
|
||||
llm_output=llm,
|
||||
metadata={"trainedWords": []},
|
||||
metadata={},
|
||||
)
|
||||
applied = mock_apply.call_args[0][1]
|
||||
assert applied["civitai"]["trainedWords"] == ["trigger1", "trigger2"]
|
||||
|
||||
@@ -227,8 +227,20 @@ function formatAutocompleteInsertion(text = '') {
|
||||
return getAutocompleteAppendCommaPreference() ? `${trimmed},` : `${trimmed} `;
|
||||
}
|
||||
|
||||
// Matches a complete <lora:name:strength[:clip_strength]> tag. Kept
|
||||
// permissive on the strength fields (mirrors the backend parser) so tags
|
||||
// are still protected while the user is mid-edit.
|
||||
const LORA_TAG_PATTERN = /(<lora:[^:>]+:[^:>]+(?::[^:>]+)?>)/gi;
|
||||
|
||||
function normalizeAutocompleteSegment(segment = '') {
|
||||
return segment.replace(/\s+/g, ' ').trim();
|
||||
// Collapse whitespace only outside <lora:...> tags: names inside the tags
|
||||
// may legitimately contain repeated spaces (e.g. "test - 0021"), and
|
||||
// collapsing them breaks file resolution at runtime.
|
||||
return segment
|
||||
.split(LORA_TAG_PATTERN)
|
||||
.map((part, index) => (index % 2 === 1 ? part : part.replace(/\s+/g, ' ')))
|
||||
.join('')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function formatAutocompleteTextOnBlur(text = '') {
|
||||
|
||||
@@ -38,7 +38,17 @@ function cleanupLoraSyntax(text) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let cleaned = text
|
||||
// Protect <lora:...> tags with placeholders before cleanup: names inside
|
||||
// the tags may legitimately contain repeated spaces or commas (e.g.
|
||||
// "test - 0021"), and collapsing them breaks file resolution at runtime.
|
||||
const protectedTags = [];
|
||||
LORA_PATTERN.lastIndex = 0;
|
||||
const masked = text.replace(LORA_PATTERN, (match) => {
|
||||
protectedTags.push(match);
|
||||
return `\u0000${protectedTags.length - 1}\u0000`;
|
||||
});
|
||||
|
||||
let cleaned = masked
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/,\s*,+/g, ",")
|
||||
.replace(/\s*,\s*/g, ",")
|
||||
@@ -51,7 +61,9 @@ function cleanupLoraSyntax(text) {
|
||||
cleaned = cleaned.replace(/(^,)|(,$)/g, "");
|
||||
cleaned = cleaned.replace(/,\s*/g, ", ");
|
||||
|
||||
return cleaned.trim();
|
||||
return cleaned
|
||||
.trim()
|
||||
.replace(/\u0000(\d+)\u0000/g, (_, index) => protectedTags[Number(index)]);
|
||||
}
|
||||
|
||||
export function applyLoraValuesToText(originalText, loras) {
|
||||
|
||||
Reference in New Issue
Block a user