fix(lora): show linked LoraManager videos as videos (#355)

_enrich_images marks every image without is_video by its extension, so an
.mp4 preview outside the output folders shows the Video preview label and
plays in the inspector. Tests cover serving the sidecar and example-image
folders only while the integration is on, and a filesystem root as the
sidecar folder adding nothing.
This commit is contained in:
Vito Sansevero
2026-10-26 17:48:30 -07:00
parent 9d8c4fafe4
commit 145b105b82
2 changed files with 120 additions and 1 deletions
+11 -1
View File
@@ -750,7 +750,12 @@ class PromptManagerAPI(
# ── Shared utilities used by multiple mixins ──────────────────────
def _enrich_images(self, images):
"""Add url and thumbnail_url to a flat list of image dicts."""
"""Add url and thumbnail_url to a flat list of image dicts.
Each dict without ``is_video`` also gets ``is_video`` and
``media_type`` from its extension (#355: a linked LoraManager video
outside the output folders is shown as a video, not a picture).
"""
from urllib.parse import quote as url_quote
output_dirs = self._get_all_output_dirs()
@@ -760,6 +765,11 @@ class PromptManagerAPI(
if not image_path_str:
continue
if "is_video" not in image:
video = is_video_path(image_path_str)
image["is_video"] = video
image["media_type"] = "video" if video else "image"
img_path = Path(image_path_str)
# Set fallback url via image ID
+109
View File
@@ -1396,6 +1396,115 @@ class TestLoraManagerPathAllowList(ImageRouteCoverageCase):
resolve.assert_called_once_with(configured)
class TestLoraManagerMediaServing(ImageRouteCoverageCase):
"""#355: LoraManager's sidecar and example-image folders are served while
the integration is on, only through the folder guard."""
async def setUpAsync(self):
await super().setUpAsync()
from py.config import IntegrationConfig
self._lm_tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.addCleanup(self._lm_tmp.cleanup)
root = Path(os.path.realpath(self._lm_tmp.name))
custom_nodes = root / "custom_nodes"
self.lm = custom_nodes / "ComfyUI-Lora-Manager"
(self.lm / "py").mkdir(parents=True)
(self.lm / "__init__.py").write_text("")
self.settings = root / "settings"
self.settings.mkdir()
self.sidecars = root / "sidecars"
self.examples = root / "examples"
self.cache = root / "cache"
for folder in (self.sidecars, self.examples, self.cache):
folder.mkdir()
self.write_settings(str(self.sidecars))
self.enabled = patch.object(IntegrationConfig, "LORA_MANAGER_ENABLED", True)
for item in (
self.enabled,
patch.object(IntegrationConfig, "LORA_MANAGER_PATH", str(self.lm)),
patch(
"py.lora_utils.custom_nodes_directories", return_value=[custom_nodes]
),
patch("py.lora_utils.default_lora_roots", return_value=[]),
patch("py.lora_utils.get_lora_image_cache_dir", return_value=self.cache),
patch.dict(os.environ, {"LORA_MANAGER_SETTINGS_DIR": str(self.settings)}),
):
item.start()
self.addCleanup(item.stop)
def write_settings(self, sidecar_root):
(self.settings / "settings.json").write_text(
json.dumps(
{
"sidecar_storage_mode": "centralized",
"sidecar_storage_path": sidecar_root,
"example_images_path": str(self.examples),
"civitai_api_key": "never-served",
}
)
)
async def _status(self, image_id):
resp = await self.client.request(
"GET", f"/prompt_manager/images/{image_id}/file"
)
await resp.read() # release the served file before the folder goes
return resp.status
async def test_sidecar_and_example_media_are_served_while_enabled(self):
preview = self.sidecars / "loras-0a1b2c3d" / "neon.webp"
preview.parent.mkdir()
preview.write_bytes(b"RIFF")
example = self.examples / "ab12" / "1.mp4"
example.parent.mkdir()
example.write_bytes(b"video")
ids = [self._link_image(preview, "a"), self._link_image(example, "b")]
for image_id in ids:
self.assertEqual(await self._status(image_id), 200)
self.enabled.stop()
for image_id in ids:
self.assertEqual(await self._status(image_id), 403)
self.enabled.start()
async def test_a_filesystem_root_as_sidecar_folder_adds_nothing(self):
self.write_settings(os.path.abspath(os.sep))
dirs = self.api._lora_image_dirs()
# An unusable layout: the default LoRA roots (none here) and the cache.
self.assertEqual(dirs, [self.cache])
async def test_the_layout_folders_are_the_served_ones(self):
dirs = self.api._lora_image_dirs()
self.assertEqual(
dirs,
[
Path(os.path.normcase(str(self.sidecars))),
Path(os.path.normcase(str(self.examples))),
self.cache,
],
)
class TestEnrichMarksVideos(ImageAPITestCase):
"""#355: a linked video outside the output folders is marked as video."""
async def test_enrich_sets_is_video_and_media_type(self):
elsewhere = Path(tempfile.gettempdir()) / "lora-media"
images = [
{"id": 1, "image_path": str(elsewhere / "a.MP4")},
{"id": 2, "image_path": str(elsewhere / "b.png")},
{"id": 3, "image_path": str(elsewhere / "c.mp4"), "is_video": False},
{"id": 4, "image_path": ""},
]
self.api._enrich_images(images)
self.assertEqual(
[(i.get("is_video"), i.get("media_type")) for i in images],
[(True, "video"), (False, "image"), (False, None), (None, None)],
)
self.assertEqual(images[0]["url"], "/prompt_manager/images/1/file")
class TestLinkImageRelativePaths(ImageAPITestCase):
"""POST /images/link accepts the path forms the API hands out."""