From 5837dcdd631a7e230b050deff4e54a52e5384d63 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Wed, 19 Aug 2026 01:39:45 +0100 Subject: [PATCH] refactor(video): carry output resolution on the video result Both runners round the requested resolution and the two-stage LTX path derives its own, so the request never recorded what came out. Read it off the decoded frames instead. --- modules/video_models/video_run.py | 8 ++++++-- modules/video_models/video_utils.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/modules/video_models/video_run.py b/modules/video_models/video_run.py index 5f36ea8d0..4efd60953 100644 --- a/modules/video_models/video_run.py +++ b/modules/video_models/video_run.py @@ -29,6 +29,8 @@ class VideoResult: has_audio: bool still: bool processed: processing.Processed + width: int = 0 # what was generated, which is not what was requested whenever a runner rounds or a model picks + height: int = 0 def resolve_model(engine: str | None, model: str | None) -> tuple[models_def.Model, bool]: @@ -302,7 +304,8 @@ def run(selected: models_def.Model, *, if getattr(p, 'video_still', False): stills = processed.images[:1] # already trimmed in process_decode; defensive - return VideoResult(images=stills, video_path=None, thumb_path=None, num_frames=len(stills), fps=0.0, has_audio=False, still=True, processed=processed) + still_w, still_h = video_utils.pixel_size(stills, fallback=(p.width, p.height)) + return VideoResult(images=stills, video_path=None, thumb_path=None, num_frames=len(stills), fps=0.0, has_audio=False, still=True, processed=processed, width=still_w, height=still_h) if hasattr(processed, 'images') and processed.images is not None: pixels = video_save.images_to_tensor(processed.images) @@ -341,8 +344,9 @@ def run(selected: models_def.Model, *, mp4_interpolate=mp4_interpolate, metadata={}, ) + out_w, out_h = video_utils.pixel_size(processed.images, fallback=(p.width, p.height)) del pixels - return VideoResult(images=processed.images, video_path=video_file, thumb_path=thumb_file, num_frames=num_frames, fps=float(save_fps), has_audio=waveform is not None, still=False, processed=processed) + return VideoResult(images=processed.images, video_path=video_file, thumb_path=thumb_file, num_frames=num_frames, fps=float(save_fps), has_audio=waveform is not None, still=False, processed=processed, width=out_w, height=out_h) def generate(task_id, ui_state, diff --git a/modules/video_models/video_utils.py b/modules/video_models/video_utils.py index 1444d4782..d667e1ecd 100644 --- a/modules/video_models/video_utils.py +++ b/modules/video_models/video_utils.py @@ -73,6 +73,26 @@ def has_torchaudio(): return False +def pixel_size(pixels, fallback: tuple[int, int] = (0, 0)) -> tuple[int, int]: + """Width and height of decoded frames, whether they arrive as PIL images or as a tensor. + + The runners request a resolution and the model answers with another one often enough that + the request is not a usable substitute, so read it off the pixels and keep the fallback for + the case where nothing was decoded at all. + """ + if isinstance(pixels, list): + return pixels[0].size if len(pixels) > 0 and hasattr(pixels[0], 'size') else fallback + ndim = getattr(pixels, 'ndim', None) + if ndim == 5: # NCTHW + return int(pixels.shape[-1]), int(pixels.shape[-2]) + if ndim == 4: # NHWC + return int(pixels.shape[2]), int(pixels.shape[1]) + shape = getattr(pixels, 'shape', None) + if shape is not None and len(shape) >= 2: + return int(shape[-1]), int(shape[-2]) + return fallback + + def classify_extension(fn: str): """Media kind of a filename, None when the extension is not one sdnext reads.""" lower = str(fn).lower()