add support for video previews (#197)

* add support for video previews

* fix two cases where video previews did not show
This commit is contained in:
Kevin Lewis
2025-08-14 22:12:13 -04:00
committed by GitHub
parent c96a164f68
commit 71a200ed5c
13 changed files with 267 additions and 120 deletions

View File

@@ -326,7 +326,7 @@ class ModelDownload:
try:
preview_file = task_data.pop("previewFile", None)
utils.save_model_preview_image(task_path, preview_file, download_platform)
utils.save_model_preview(task_path, preview_file, download_platform)
self.set_task_content(task_id, task_data)
task_status = TaskStatus(
taskId=task_id,

View File

@@ -355,23 +355,17 @@ class Information:
@routes.get("/model-manager/preview/{type}/{index}/{filename:.*}")
async def read_model_preview(request):
"""
Get the file stream of the specified image.
Get the file stream of the specified preview
If the file does not exist, no-preview.png is returned.
:param type: The type of the model. eg.checkpoints, loras, vae, etc.
:param index: The index of the model folders.
:param filename: The filename of the image.
:param filename: The filename of the preview.
"""
model_type = request.match_info.get("type", None)
index = int(request.match_info.get("index", None))
filename = request.match_info.get("filename", None)
content_type = utils.resolve_file_content_type(filename)
if content_type == "video":
abs_path = utils.get_full_path(model_type, index, filename)
return web.FileResponse(abs_path)
extension_uri = config.extension_uri
try:
@@ -388,8 +382,16 @@ class Information:
if not os.path.isfile(abs_path):
abs_path = utils.join_path(extension_uri, "assets", "no-preview.png")
image_data = self.get_image_preview_data(abs_path)
return web.Response(body=image_data.getvalue(), content_type="image/webp")
# Determine content type from the actual file
content_type = utils.resolve_file_content_type(abs_path)
if content_type == "video":
# Serve video files directly
return web.FileResponse(abs_path)
else:
# Serve image files (WebP or fallback images)
image_data = self.get_image_preview_data(abs_path)
return web.Response(body=image_data.getvalue(), content_type="image/webp")
@routes.get("/model-manager/preview/download/{filename}")
async def read_download_preview(request):
@@ -546,10 +548,10 @@ class Information:
model_info = CivitaiModelSearcher().search_by_hash(hash_value)
preview_url_list = model_info.get("preview", [])
preview_image_url = preview_url_list[0] if preview_url_list else None
if preview_image_url:
utils.print_debug(f"Save preview image to {abs_image_path}")
utils.save_model_preview_image(abs_model_path, preview_image_url)
preview_url = preview_url_list[0] if preview_url_list else None
if preview_url:
utils.print_debug(f"Save preview to {abs_model_path}")
utils.save_model_preview(abs_model_path, preview_url)
description = model_info.get("description", None)
if description:

View File

@@ -134,18 +134,8 @@ class ModelManager:
if is_file and extension not in folder_paths.supported_pt_extensions:
return None
preview_type = "image"
preview_ext = ".webp"
preview_images = utils.get_model_all_images(entry.path)
if len(preview_images) > 0:
preview_type = "image"
preview_ext = ".webp"
else:
preview_videos = utils.get_model_all_videos(entry.path)
if len(preview_videos) > 0:
preview_type = "video"
preview_ext = f".{preview_videos[0].split('.')[-1]}"
preview_name = utils.get_model_preview_name(entry.path)
preview_ext = f".{preview_name.split('.')[-1]}"
model_preview = f"/model-manager/preview/{folder}/{path_index}/{relative_path.replace(extension, preview_ext)}"
stat = entry.stat()
@@ -158,7 +148,6 @@ class ModelManager:
"pathIndex": path_index,
"sizeBytes": stat.st_size if is_file else 0,
"preview": model_preview if is_file else None,
"previewType": preview_type,
"createdAt": round(stat.st_ctime_ns / 1000000),
"updatedAt": round(stat.st_mtime_ns / 1000000),
}
@@ -211,10 +200,11 @@ class ModelManager:
if "previewFile" in model_data:
previewFile = model_data["previewFile"]
if type(previewFile) is str and previewFile == "undefined":
utils.remove_model_preview_image(model_path)
else:
utils.save_model_preview_image(model_path, previewFile)
# Always remove existing preview files first in case the file extension has changed
utils.remove_model_preview(model_path)
# Nothing else to do if the preview file was being removed
if not (type(previewFile) is str and previewFile == "undefined"):
utils.save_model_preview(model_path, previewFile)
if "description" in model_data:
description = model_data["description"]
@@ -236,7 +226,7 @@ class ModelManager:
model_dirname = os.path.dirname(model_path)
os.remove(model_path)
model_previews = utils.get_model_all_images(model_path)
model_previews = utils.get_model_all_previews(model_path)
for preview in model_previews:
os.remove(utils.join_path(model_dirname, preview))

View File

@@ -17,6 +17,22 @@ from aiohttp import web
from typing import Any, Optional
from . import config
# Media file extensions
VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.m4v', '.ogv']
IMAGE_EXTENSIONS = ['.webp', '.png', '.jpg', '.jpeg', '.gif', '.bmp']
# Content type mappings
VIDEO_CONTENT_TYPE_MAP = {
'video/mp4': '.mp4',
'video/webm': '.webm',
'video/quicktime': '.mov',
'video/x-msvideo': '.avi',
'video/x-matroska': '.mkv',
'video/x-flv': '.flv',
'video/x-ms-wmv': '.wmv',
'video/ogg': '.ogv',
}
def print_info(msg, *args, **kwargs):
logging.info(f"[{config.extension_tag}] {msg}", *args, **kwargs)
@@ -252,10 +268,10 @@ def get_model_metadata(filename: str):
return {}
def get_model_all_images(model_path: str):
def get_model_all_previews(model_path: str):
base_dirname = os.path.dirname(model_path)
files = search_files(base_dirname)
files = folder_paths.filter_files_content_types(files, ["image"])
files = folder_paths.filter_files_content_types(files, ["video", "image"])
basename = os.path.splitext(os.path.basename(model_path))[0]
output: list[str] = []
@@ -269,78 +285,135 @@ def get_model_all_images(model_path: str):
def get_model_preview_name(model_path: str):
images = get_model_all_images(model_path)
basename = os.path.splitext(os.path.basename(model_path))[0]
for image in images:
image_name = os.path.splitext(image)[0]
image_ext = os.path.splitext(image)[1]
if image_name == basename and image_ext.lower() == ".webp":
return image
return images[0] if len(images) > 0 else "no-preview.png"
def get_model_all_videos(model_path: str):
"""
Get the preview file name for a model. Checks for images and videos in all supported formats.
Returns the first available preview file or 'no-preview.png' if none found.
"""
base_dirname = os.path.dirname(model_path)
files = search_files(base_dirname)
files = folder_paths.filter_files_content_types(files, ["video"])
basename = os.path.splitext(os.path.basename(model_path))[0]
output: list[str] = []
for file in files:
file_basename = os.path.splitext(file)[0]
if file_basename == basename:
output.append(file)
if file_basename == f"{basename}.preview":
output.append(file)
return output
# Prefer previews with these extensions in this order
preview_extensions = ['.webm', '.mp4', '.webp']
for ext in preview_extensions:
preview_name = f"{basename}{ext}"
if os.path.exists(join_path(base_dirname, preview_name)):
return preview_name
# Fallback to any available preview files
all_previews = get_model_all_previews(model_path)
return all_previews[0] if len(all_previews) > 0 else "no-preview.png"
from PIL import Image
from io import BytesIO
def remove_model_preview_image(model_path: str):
def remove_model_preview(model_path: str):
"""
Remove preview files for a model.
"""
basename = os.path.splitext(model_path)[0]
preview_path = f"{basename}.webp"
if os.path.exists(preview_path):
os.remove(preview_path)
base_dirname = os.path.dirname(model_path)
# Remove all preview files
for ext in VIDEO_EXTENSIONS + IMAGE_EXTENSIONS:
preview_path = f"{basename}{ext}"
if os.path.exists(preview_path):
os.remove(preview_path)
# Also check for .preview variants
files = search_files(base_dirname)
model_name = os.path.splitext(os.path.basename(model_path))[0]
for file in files:
if file.startswith(f"{model_name}.preview"):
file_path = join_path(base_dirname, file)
if os.path.exists(file_path):
os.remove(file_path)
def save_model_preview_image(model_path: str, image_file_or_url: Any, platform: Optional[str] = None):
def save_model_preview(model_path: str, file_or_url: Any, platform: Optional[str] = None):
"""
Save a preview file for a model.
Images are converted to WebP, videos are saved in their original format.
"""
basename = os.path.splitext(model_path)[0]
preview_path = f"{basename}.webp"
# Download image file if it is url
if type(image_file_or_url) is str:
image_url = image_file_or_url
# Download file if it is a URL
if type(file_or_url) is str:
url = file_or_url
try:
image_response = requests.get(image_url)
image_response.raise_for_status()
image = Image.open(BytesIO(image_response.content))
image.save(preview_path, "WEBP")
response = requests.get(url)
response.raise_for_status()
# Determine content type from response headers or URL extension
content_type = response.headers.get('content-type', '')
if not content_type:
# Fallback to URL extension detection
content_type = resolve_file_content_type(url) or ''
content = response.content
if content_type.startswith("video/"):
# Save video in original format
# Try to get extension from URL or content-type
ext = _get_video_extension_from_url(url) or _get_extension_from_content_type(content_type)
if not ext:
ext = '.mp4' # Default fallback
preview_path = f"{basename}{ext}"
with open(preview_path, 'wb') as f:
f.write(content)
else:
# Default to image processing for unknown or image types
preview_path = f"{basename}.webp"
image = Image.open(BytesIO(content))
image.save(preview_path, "WEBP")
except Exception as e:
print_error(f"Failed to download image: {e}")
print_error(f"Failed to download preview: {e}")
# Handle uploaded file
else:
# Assert image as file
image_file = image_file_or_url
file_obj = file_or_url
if not isinstance(image_file, web.FileField):
raise RuntimeError("Invalid image file")
if not isinstance(file_obj, web.FileField):
raise RuntimeError("Invalid file")
content_type: str = image_file.content_type
if not content_type.startswith("image/"):
if platform == "huggingface":
# huggingface previewFile content_type='text/plain', not startswith("image/")
return
else:
raise RuntimeError(f"FileTypeError: expected image, got {content_type}")
image = Image.open(image_file.file)
image.save(preview_path, "WEBP")
content_type: str = file_obj.content_type
filename: str = getattr(file_obj, 'filename', '')
if content_type.startswith("video/"):
# Save video in original format for now, consider transcoding to webm to follow the pattern for images converting to webp
ext = os.path.splitext(filename.lower())[1]
if not ext:
ext = '.mp4' # Default fallback
preview_path = f"{basename}{ext}"
file_obj.file.seek(0)
content = file_obj.file.read()
with open(preview_path, 'wb') as f:
f.write(content)
elif content_type.startswith("image/"):
# Convert image to webp
preview_path = f"{basename}.webp"
image = Image.open(file_obj.file)
image.save(preview_path, "WEBP")
else:
raise RuntimeError(f"FileTypeError: expected image or video, got {content_type}")
def _get_video_extension_from_url(url: str) -> Optional[str]:
"""Extract video extension from URL."""
from urllib.parse import urlparse
path = urlparse(url).path.lower()
for ext in VIDEO_EXTENSIONS:
if path.endswith(ext):
return ext
return None
def _get_extension_from_content_type(content_type: str) -> Optional[str]:
"""Map content-type to file extension."""
return VIDEO_CONTENT_TYPE_MAP.get(content_type.lower())
def get_model_all_descriptions(model_path: str):
@@ -398,7 +471,7 @@ def rename_model(model_path: str, new_model_path: str):
shutil.move(model_path, new_model_path)
# move preview
previews = get_model_all_images(model_path)
previews = get_model_all_previews(model_path)
for preview in previews:
preview_path = join_path(model_dirname, preview)
preview_name = os.path.splitext(preview)[0]