feat(lora): support relative paths in <lora:folder/name:strength> syntax (#917)

Autocomplete, copy/send-to-workflow, and recipe syntax now emit
<lora:folder/name:strength> instead of <lora:name:strength>, using
relative paths to disambiguate identically-named loras in different
subfolders without requiring file renames.

Backend: 3-tier hybrid resolution (path → bare → basename fallback)
across get_lora_info, get_lora_info_absolute, get_model_preview_url,
get_model_civitai_url, get_model_info_by_name, get_lora_metadata_by_filename,
and get_hash_by_filename. Also fix get_random_loras and get_cycler_list
to return path-prefixed names for randomizer/cycler consistency.

Frontend: autocomplete, copyLoraSyntax, handleSendToWorkflow emit
folder-prefixed syntax. extract_lora_name preserves relative paths.

Saved image metadata (<lora:...> in EXIF) intentionally keeps basename-only
for compatibility with A1111/Forge ecosystem.
This commit is contained in:
Will Miao
2026-05-20 19:39:12 +08:00
parent 33e5f3d85d
commit 9ce56dd40c
13 changed files with 404 additions and 48 deletions

View File

@@ -185,7 +185,7 @@ describe('AutoComplete widget interactions', () => {
expect(fetchApiMock).toHaveBeenCalledWith(
'/lm/loras/usage-tips-by-path?relative_path=models%2Fexample.safetensors',
);
expect(input.value).toContain('<lora:example:1.5:0.9>,');
expect(input.value).toContain('<lora:models/example:1.5:0.9>,');
expect(autoComplete.dropdown.style.display).toBe('none');
expect(input.focus).toHaveBeenCalled();
expect(input.setSelectionRange).toHaveBeenCalled();
@@ -1624,8 +1624,8 @@ describe('AutoComplete widget interactions', () => {
await autoComplete.insertSelection('models/example.safetensors');
expect(input.value).toContain('<lora:example:1.2>');
expect(input.value).not.toContain('<lora:example:1.2>,');
expect(input.value).toContain('<lora:models/example:1.2>');
expect(input.value).not.toContain('<lora:models/example:1.2>,');
});
it('replaces entire phrase when selected tag ends with underscore version of search term (suffix match)', async () => {