feat: enrich LoRA sidebar with shared media
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user