feat: enrich LoRA sidebar with shared media

This commit is contained in:
ethanfel
2026-08-24 23:02:08 +02:00
parent 5bb3b3a349
commit 932272806c
7 changed files with 1371 additions and 20 deletions
@@ -5,8 +5,12 @@ import {
buildExternalLinks,
extractLoraNames,
getSelectedGraphNodes,
isVideoMedia,
matchModelItems,
mergeCivitaiMetadata,
normalizeLoraIdentifier,
normalizeMediaSettings,
normalizeSharedMedia,
normalizeUsageTips,
} from "../../web/comfyui/lora_manager_sidebar_utils.js";
@@ -31,6 +35,168 @@ test("formats Manager usage presets and hides empty JSON", () => {
]);
});
test("detects direct and encoded video preview URLs", () => {
assert.equal(
isVideoMedia(
"/api/lm/previews?path=%2Fmodels%2Floras%2Fexample.MP4"
),
true
);
assert.equal(isVideoMedia("https://example.com/demo.WEBM?download=1"), true);
assert.equal(isVideoMedia("https://example.com/media/42", "video"), true);
assert.equal(isVideoMedia("https://example.com/still.png"), false);
assert.doesNotThrow(() => isVideoMedia("/preview?path=%E0%A4%A"));
});
test("merges full Civitai metadata without losing resolve fields", () => {
assert.deepEqual(
mergeCivitaiMetadata(
{ id: 7, modelId: 9, trainedWords: ["portrait"] },
{ description: "Full details", images: [{ id: 1 }] }
),
{
id: 7,
modelId: 9,
trainedWords: ["portrait"],
description: "Full details",
images: [{ id: 1 }],
}
);
});
test("normalizes community and Civitai media with associated prompts", () => {
const media = normalizeSharedMedia(
[
{
civitai_image_id: 10,
preview_url:
"/api/lm/previews?path=%2Fexamples%2Fcommunity-video.mp4",
media_type: "video",
prompt: "community prompt",
negative_prompt: "community negative",
username: "artist",
steps: 20,
cfg_scale: 4.5,
width: 1280,
height: 720,
},
],
{
images: [
{
id: 10,
url: "https://example.com/duplicate.mp4",
type: "video",
nsfwLevel: 16,
meta: { prompt: "duplicate prompt" },
},
{
id: 11,
url: "https://example.com/still.webp",
meta: {
meta: {
prompt: "example prompt",
negativePrompt: "example negative",
sampler: "Euler",
seed: 12,
Size: "640x480",
Model: "Example checkpoint",
clipSkip: 2,
},
},
},
],
customImages: [
{ id: 12, url: "javascript:alert(1)", meta: { prompt: "unsafe" } },
{
id: 14,
url: "https://user:password@example.com/private.webp",
meta: { prompt: "credential leak" },
},
{
id: 13,
url: "https://example.com/failed.webp",
downloadFailed: true,
},
{
id: "local-one",
url: "",
meta: { prompt: "local custom prompt" },
},
],
},
[
{
name: "custom_local-one.webm",
path: "/example_images_static/abc/custom_local-one.webm",
is_video: true,
},
]
);
assert.equal(media.length, 3);
assert.deepEqual(
{
source: media[0].source,
mediaType: media[0].mediaType,
prompt: media[0].prompt,
negativePrompt: media[0].negativePrompt,
username: media[0].username,
steps: media[0].steps,
cfgScale: media[0].cfgScale,
},
{
source: "Community creation",
mediaType: "video",
prompt: "community prompt",
negativePrompt: "community negative",
username: "artist",
steps: 20,
cfgScale: 4.5,
}
);
assert.equal(media[0].url.includes("/api/lm/previews"), true);
assert.equal(media[0].nsfwLevel, 16);
assert.equal(media[1].prompt, "example prompt");
assert.equal(media[1].negativePrompt, "example negative");
assert.equal(media[1].sampler, "Euler");
assert.equal(media[1].seed, 12);
assert.equal(media[1].width, 640);
assert.equal(media[1].height, 480);
assert.equal(media[1].modelName, "Example checkpoint");
assert.equal(media[1].clipSkip, 2);
assert.equal(media[2].url, "/example_images_static/abc/custom_local-one.webm");
assert.equal(media[2].mediaType, "video");
assert.equal(media[2].prompt, "local custom prompt");
});
test("optimizes Civitai media and normalizes mature-content settings", () => {
const media = normalizeSharedMedia([], {
images: [
{
id: 1,
url: "https://image.civitai.com/example/original=true/video.mp4",
type: "video",
},
],
});
assert.equal(media.length, 1);
assert.match(media[0].url, /transcode=true,width=450,optimized=true/);
assert.deepEqual(
normalizeMediaSettings({
blur_mature_content: false,
mature_blur_level: "XXX",
show_only_sfw: true,
}),
{
blurMatureContent: false,
matureBlurLevel: 16,
showOnlySfw: true,
}
);
});
test("extracts stock and numbered LoRA loader widgets", () => {
const node = {
comfyClass: "Power Lora Loader",
+98
View File
@@ -4,6 +4,7 @@ import importlib.util
import json
import sys
import types
from contextlib import asynccontextmanager
from pathlib import Path
import pytest
@@ -508,6 +509,103 @@ class FakeSession:
self.closed = True
@pytest.mark.asyncio
async def test_media_proxy_streams_range_response_without_buffering(
isolated_proxy, modules, monkeypatch
):
proxy, config = isolated_proxy
captured = {}
class UpstreamContent:
async def iter_chunked(self, size):
assert size == proxy._MEDIA_STREAM_CHUNK_SIZE
for chunk in (b"abc", b"def"):
yield chunk
class UpstreamResponse:
status = 206
headers = {
"Content-Type": "video/mp4",
"Content-Length": "6",
"Content-Range": "bytes 0-5/20",
"Accept-Ranges": "bytes",
"Set-Cookie": "remote=secret",
}
content = UpstreamContent()
read_called = False
async def read(self):
self.read_called = True
raise AssertionError("streaming media must not call read()")
upstream = UpstreamResponse()
class RequestContext:
async def __aenter__(self):
return upstream
async def __aexit__(self, *_args):
return None
class StreamingSession:
def request(self, **kwargs):
captured["request"] = kwargs
return RequestContext()
@asynccontextmanager
async def fake_lease(snapshot):
captured["generation"] = snapshot.generation
yield StreamingSession()
class DownstreamResponse:
def __init__(self, *, status, headers):
self.status = status
self.headers = headers
self.prepared = False
self.chunks = []
self.eof = False
async def prepare(self, request):
self.prepared = True
self.request = request
async def write(self, chunk):
self.chunks.append(chunk)
async def write_eof(self):
self.eof = True
monkeypatch.setattr(proxy, "_proxy_session_lease", fake_lease)
monkeypatch.setattr(proxy.web, "StreamResponse", DownstreamResponse)
snapshot = modules.config.ConfigSnapshot(
config.generation,
"http://manager.local:8188",
30,
(),
)
request = DummyRequest(
path="/api/lm/previews",
headers={"Range": "bytes=0-"},
)
response = await proxy._proxy_http(request, snapshot)
assert response.status == 206
assert response.chunks == [b"abc", b"def"]
assert response.eof is True
assert upstream.read_called is False
assert response.headers["Content-Range"] == "bytes 0-5/20"
assert response.headers["Accept-Ranges"] == "bytes"
assert response.headers["Content-Length"] == "6"
assert "Set-Cookie" not in response.headers
assert captured["request"]["headers"]["Range"] == "bytes=0-"
assert captured["request"]["auto_decompress"] is False
timeout = captured["request"]["timeout"]
assert timeout.total is None
assert timeout.sock_connect == 30
assert timeout.sock_read == proxy._MEDIA_IDLE_TIMEOUT
@pytest.mark.asyncio
async def test_proxy_session_does_not_retain_remote_cookies(
isolated_proxy, monkeypatch