From efe281c757cb460cdf5fbfee1f1ebd273a73fee4 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:43:42 +0800 Subject: [PATCH] Never send a video as its own gallery card When a video's first frame could not be read (a broken file, no decoder) the card's thumbnail route served the whole video as the picture. It now gets the same SVG placeholder with its size as a picture with no card. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- api/gallery_routes.py | 6 +++--- docs/architecture/backend.md | 2 +- tests/test_card_images.py | 16 ++++++++++++++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4009e85..0974478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,8 @@ - 跑图前的尺寸提醒多算了一步:放大模型之后**最后要存多大**。要存出超过约 6400 万像素的图(比如 5120×3328 再放大 4 倍,存出来 20480×13312、PNG 约 300 MB),时间线上会出现黄色提醒,AI 也会收到,建议放大倍数小一点或者从小一点的图开始放大。 - The size check before a run now also counts **what is finally saved** after an upscale model: a picture past about 64 megapixels (5120×3328 upscaled 4x is 20480×13312, about 300 MB as PNG) gets a yellow warning on the timeline, and the AI is told, with the advice to upscale less or start from a smaller picture. -- **超大图不再卡住图库**:几亿像素的图(比如把大图再放大 4 倍,存出来几百 MB),以前缩略图做不出来时,AMB 会把整张原图当缩略图发给浏览器,图库和 AI 工作台一翻到它就卡。现在缩略图在后台单独的进程里做一次就存下来(2.7 亿像素的图约 6 秒),不占 ComfyUI 出图的内存;大到连这样也做不了的,卡片上显示尺寸和大小,不再加载原图。卡片角上标“超大图 · 324 MB”。点开看大图、图片详情、AI 工作台里放大看的,都是缩到 4096 像素的预览,下面写着原图多大,要看原图点“打开原图”(在浏览器新标签页里打开)。图片详情读参数也不再把整张图下载下来。 -- **Very large pictures no longer freeze the gallery**: for a picture of hundreds of megapixels (a big one upscaled 4x, hundreds of MB), a card image that could not be made used to be the whole original, sent to the browser every time the gallery or the AI workbench showed it. Now it is made once in a separate background process and kept (about 6 s for 272 megapixels), outside ComfyUI's own memory; one too large even for that shows its size instead, never the original. Its card says "Very large · 324 MB". The large view, the image details and the AI workbench's larger pictures show a 4096 px preview with a note saying how large the original is, and "Open the original" opens it in a new browser tab. Reading a picture's settings in its details no longer downloads the whole file. +- **超大图不再卡住图库**:几亿像素的图(比如把大图再放大 4 倍,存出来几百 MB),以前缩略图做不出来时,AMB 会把整张原图当缩略图发给浏览器,图库和 AI 工作台一翻到它就卡。现在缩略图在后台单独的进程里做一次就存下来(2.7 亿像素的图约 6 秒),不占 ComfyUI 出图的内存;大到连这样也做不了的,卡片上显示尺寸和大小,不再加载原图;截不出第一帧的视频(文件坏了、格式解不开)也一样,卡片显示文件大小,不再把整个视频发给浏览器。卡片角上标“超大图 · 324 MB”。点开看大图、图片详情、AI 工作台里放大看的,都是缩到 4096 像素的预览,下面写着原图多大,要看原图点“打开原图”(在浏览器新标签页里打开)。图片详情读参数也不再把整张图下载下来。 +- **Very large pictures no longer freeze the gallery**: for a picture of hundreds of megapixels (a big one upscaled 4x, hundreds of MB), a card image that could not be made used to be the whole original, sent to the browser every time the gallery or the AI workbench showed it. Now it is made once in a separate background process and kept (about 6 s for 272 megapixels), outside ComfyUI's own memory; one too large even for that shows its size instead, never the original, and so does a video whose first frame cannot be read (a broken file, no decoder). Its card says "Very large · 324 MB". The large view, the image details and the AI workbench's larger pictures show a 4096 px preview with a note saying how large the original is, and "Open the original" opens it in a new browser tab. Reading a picture's settings in its details no longer downloads the whole file. - 做法按用途合并成 7 个:局部重画、换背景、放大、换模型精修、照着画、修脸修手、图片处理。AI 只说"用途 + 想要哪种"(比如"放大,2x"、"照着画,姿势"),AMB 按你装了什么、画布上是什么模型,自动挑能跑的、效果最好的那一块;没装的照样按节点包列出,AI 能告诉你该装什么。开新任务时 AI 只看到 7 行,做法再多也不会变长。 - The ways are grouped into 7 uses: repaint, new background, upscale, redraw with another model, follow a picture, faces and hands, picture work. The AI names a use and the kind it wants ("upscale, 2x", "follow, pose") and Anomalous picks the best block this ComfyUI can run, by what is installed and the canvas's model; the missing ones are still listed by pack, so the AI can say what to install. A new task's directory is 7 lines, however many ways there are. diff --git a/api/gallery_routes.py b/api/gallery_routes.py index 109c8dd..f4a810c 100644 --- a/api/gallery_routes.py +++ b/api/gallery_routes.py @@ -159,9 +159,9 @@ async def api_output_thumbnail(request): if served is None: return picture_placeholder(file_path) elif ext in CARD_POSTER_VIDEO_EXTENSIONS: - poster = await asyncio.to_thread(_build_video_poster, file_path) - if poster: - served = poster + served = await asyncio.to_thread(_build_video_poster, file_path) + if not served: # no first frame (a broken file, no decoder): never the video itself as a picture + return picture_placeholder(file_path) # Revalidate (cheap 304s): an output file can be replaced under the same name. return web.FileResponse(served, headers={'Cache-Control': 'no-cache'}) diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index a9791ec..969b60b 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -106,7 +106,7 @@ material (deduplicated by node type and values like other parameter materials). The former mixed utility routes are separated: `media_routes.py` owns card thumbnails and model/output media lookups (a picture past Pillow's pixel limit is shrunk once by a child process, `huge_picture.py`; a card is never a large original, a picture with none -gets an SVG placeholder with its size), `gallery_routes.py` owns the bounded +or a video with no readable first frame gets an SVG placeholder with its size), `gallery_routes.py` owns the bounded output snapshot (with each file's size, and the pixel size of big ones), the viewers' `output_preview` (the picture, or a cached 4096 px copy of a very large one) and deletion (searched through `image_search.py`, which reads PNG text chunks only and caches per-image records by mtime), `translation_routes.py` diff --git a/tests/test_card_images.py b/tests/test_card_images.py index 3956b16..59226f5 100644 --- a/tests/test_card_images.py +++ b/tests/test_card_images.py @@ -121,6 +121,22 @@ class CardImageTests(unittest.TestCase): self.assertEqual((listed['big.png']['width'], listed['big.png']['height']), (900, 600)) self.assertGreater(listed['small.png']['bytes'], 0) + def test_a_video_without_a_first_frame_gets_a_placeholder_not_itself(self): + from aiohttp.test_utils import make_mocked_request + from api import gallery_routes + output = os.path.join(self.root, 'output') + os.makedirs(output) + with open(os.path.join(output, 'broken.mp4'), 'wb') as handle: + handle.write(b'not a video' * 1000) + write_video(os.path.join(output, 'good.mp4')) + ask = lambda name: asyncio.run(gallery_routes.api_output_thumbnail(make_mocked_request('GET', f'/anomalous/output_thumbnail?filename={name}'))) + with mock.patch.object(gallery_routes.folder_paths, 'get_output_directory', return_value=output): + broken = ask('broken.mp4') + self.assertEqual(broken.content_type, 'image/svg+xml') + self.assertIn('0 MB', broken.body.decode('utf-8')) + with Image.open(ask('good.mp4')._path) as poster: + self.assertEqual(poster.format, 'WEBP') + def test_warm_up_makes_the_images_in_the_background(self): cover = os.path.join(self.root, 'a.png') Image.new('RGB', (900, 900)).save(cover)