mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
fix(example-images): read real dimensions for imported videos, fixes #1115
Example videos added through the "Add examples" flow were stored with a hardcoded 720x1280 entry. The dimension probe next to it only ran for images (PIL cannot open .mp4/.webm files), so every video entry stayed portrait regardless of the source. The showcase viewer then sizes its container straight from that value (--media-aspect in showcase.css), so landscape clips were letterboxed inside a 9:16 box. CivitAI-sourced examples were unaffected because their dimensions come from the API. PIL cannot read video containers, so add a dependency-free reader that parses the container headers instead: moov/trak/tkhd for ISO base media (with the sample description as a fallback), Segment/Tracks/Pixel* for WebM/Matroska, and RIFF/WebP for animated examples saved with a video extension. The sniffed signature decides which reader runs, so a .mp4 that is really WebM still reports the right size; the extension is only a fallback. Both readers seek past mdat rather than reading it, so a large file costs the same as a small one. Imported entries now record the file's real size and keep the previous placeholder only when the file cannot be parsed. Existing libraries keep their wrong entries, so backfill them once via the existing naming migration: bump CURRENT_NAMING_VERSION to 3 and repair each model's empty-url entries from the files on disk, then sync the scanner cache. Only entries with no remote url are touched -- those have no other source, which makes the rewrite lossless -- and entries already carrying the right size are left byte-identical, so the pass is idempotent and a no-op for libraries that never imported a video.
This commit is contained in:
@@ -15,12 +15,20 @@ from ..utils.example_images_paths import (
|
||||
)
|
||||
from ..utils.metadata_manager import MetadataManager
|
||||
from ..utils.example_images_processor import ExampleImagesProcessor
|
||||
from ..utils.example_images_metadata import update_cache_from_metadata
|
||||
from ..utils.example_images_metadata import (
|
||||
repair_local_video_dimensions,
|
||||
update_cache_from_metadata,
|
||||
)
|
||||
from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CURRENT_NAMING_VERSION = 2 # Increment this when naming conventions change
|
||||
CURRENT_NAMING_VERSION = 3 # Increment this when naming conventions change
|
||||
|
||||
# Example files worth inspecting during the dimension repair.
|
||||
_REPAIRABLE_EXTENSIONS = frozenset(
|
||||
SUPPORTED_MEDIA_EXTENSIONS["images"] + SUPPORTED_MEDIA_EXTENSIONS["videos"]
|
||||
)
|
||||
|
||||
|
||||
class _SettingsProxy:
|
||||
@@ -185,6 +193,9 @@ class ExampleImagesMigration:
|
||||
if from_version < 2 and to_version >= 2:
|
||||
await ExampleImagesMigration._migrate_to_v2(model_folders)
|
||||
|
||||
if from_version < 3 and to_version >= 3:
|
||||
await ExampleImagesMigration._migrate_to_v3(example_images_path, model_folders)
|
||||
|
||||
# Update version in progress file
|
||||
progress_file = os.path.join(example_images_path, '.download_progress.json')
|
||||
try:
|
||||
@@ -437,4 +448,137 @@ class ExampleImagesMigration:
|
||||
logger.error(f"Error migrating folder {folder}: {e}")
|
||||
migration_errors += 1
|
||||
|
||||
logger.info(f"Migration to v2 complete: migrated {count} custom examples across {updated_models} models with {migration_errors} errors")
|
||||
logger.info(f"Migration to v2 complete: migrated {count} custom examples across {updated_models} models with {migration_errors} errors")
|
||||
|
||||
@staticmethod
|
||||
def _build_local_file_map(folder):
|
||||
"""Map entry markers to their files inside a model's example folder.
|
||||
|
||||
Keys are the marker alone (``custom_<id>`` → ``<id>``,
|
||||
``image_<index>`` → ``<index>``) so they line up with the metadata
|
||||
entries' ``id``/positional index without any prefix ambiguity.
|
||||
"""
|
||||
|
||||
local_files = {}
|
||||
try:
|
||||
entries = os.listdir(folder)
|
||||
except OSError as exc:
|
||||
logger.debug("Could not list example folder %s: %s", folder, exc)
|
||||
return local_files
|
||||
|
||||
for name in entries:
|
||||
stem, ext = os.path.splitext(name)
|
||||
if ext.lower() not in _REPAIRABLE_EXTENSIONS:
|
||||
continue
|
||||
if stem.startswith("custom_"):
|
||||
local_files[stem[len("custom_"):]] = os.path.join(folder, name)
|
||||
elif stem.startswith("image_"):
|
||||
local_files[stem[len("image_"):]] = os.path.join(folder, name)
|
||||
|
||||
return local_files
|
||||
|
||||
@staticmethod
|
||||
async def _find_scanner_for_hash(model_hash):
|
||||
"""Return the scanner owning ``model_hash``, or ``None``."""
|
||||
|
||||
lora_scanner = await ServiceRegistry.get_lora_scanner()
|
||||
checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner()
|
||||
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
|
||||
|
||||
for scanner in (lora_scanner, checkpoint_scanner, embedding_scanner):
|
||||
if scanner is None:
|
||||
continue
|
||||
try:
|
||||
if scanner.has_hash(model_hash):
|
||||
return scanner
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
logger.debug("has_hash check failed for %s: %s", type(scanner).__name__, exc)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _migrate_to_v3(example_images_path, model_folders):
|
||||
"""Backfill real dimensions for locally imported example videos.
|
||||
|
||||
Imported videos were stored with a hardcoded ``720x1280`` placeholder
|
||||
(issue #1115), so landscape clips were rendered inside a portrait
|
||||
container. Only entries with an empty ``url`` are touched — those have
|
||||
no remote source, which makes the local file authoritative and the
|
||||
rewrite lossless. Entries already carrying the right size are left
|
||||
untouched, so re-running this migration is a no-op.
|
||||
|
||||
This runs once per library via the ``naming_version`` gate in
|
||||
``run_migrations``; it is deliberately not wired into any request path.
|
||||
"""
|
||||
|
||||
repaired_entries = 0
|
||||
updated_models = 0
|
||||
migration_errors = 0
|
||||
|
||||
logger.info(
|
||||
"Starting v3 migration (local example video dimensions) for %d model folders",
|
||||
len(model_folders),
|
||||
)
|
||||
|
||||
for folder in model_folders:
|
||||
try:
|
||||
model_hash = os.path.basename(folder)
|
||||
if not model_hash or len(model_hash) != 64:
|
||||
continue
|
||||
|
||||
local_files = ExampleImagesMigration._build_local_file_map(folder)
|
||||
if not local_files:
|
||||
continue
|
||||
|
||||
scanner = await ExampleImagesMigration._find_scanner_for_hash(model_hash)
|
||||
if scanner is None:
|
||||
logger.debug(
|
||||
"Model %s not found in any scanner cache, skipping dimension repair",
|
||||
model_hash,
|
||||
)
|
||||
continue
|
||||
|
||||
cache = await scanner.get_cached_data()
|
||||
model_data = None
|
||||
for item in cache.raw_data:
|
||||
if item.get("sha256") == model_hash:
|
||||
model_data = item
|
||||
break
|
||||
|
||||
if not model_data:
|
||||
continue
|
||||
|
||||
file_path = model_data.get("file_path")
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
payload = await MetadataManager.load_metadata_payload(file_path)
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
|
||||
repaired = repair_local_video_dimensions(payload, local_files)
|
||||
if repaired <= 0:
|
||||
continue
|
||||
|
||||
# The model cache shape differs from the on-disk payload, so
|
||||
# persist the file first and let the cache sync re-read it.
|
||||
await MetadataManager.save_metadata(file_path, payload)
|
||||
await update_cache_from_metadata(scanner, file_path, payload)
|
||||
|
||||
repaired_entries += repaired
|
||||
updated_models += 1
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to repair example video dimensions for %s: %s",
|
||||
folder,
|
||||
exc,
|
||||
)
|
||||
migration_errors += 1
|
||||
|
||||
logger.info(
|
||||
"Migration to v3 complete: repaired %d example entr(ies) across %d model(s) "
|
||||
"with %d error(s)",
|
||||
repaired_entries,
|
||||
updated_models,
|
||||
migration_errors,
|
||||
)
|
||||
Reference in New Issue
Block a user