From 61a7494e58d97b0aac7c497d73dc8506c1768aee Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 00:57:56 +0800 Subject: [PATCH] fix: migrate SeedVR and SwiftVR to LightX2V 0.6 API --- nodes/_lightx2v_runner.py | 53 ++++++++++++++++ nodes/seedvr.py | 130 +++++++++++++------------------------- nodes/swiftvr.py | 99 ++++++++++------------------- 3 files changed, 133 insertions(+), 149 deletions(-) create mode 100644 nodes/_lightx2v_runner.py diff --git a/nodes/_lightx2v_runner.py b/nodes/_lightx2v_runner.py new file mode 100644 index 0000000..03c11f3 --- /dev/null +++ b/nodes/_lightx2v_runner.py @@ -0,0 +1,53 @@ +"""Small adapters for LightX2V 0.6's startup/request runner API.""" + +from contextlib import contextmanager + + +def build_runner(startup_data): + """Build a resident LightX2V runner from deployment-time settings.""" + from ..lightx2v.lightx2v.models.runners.runner_factory import build_runner as _build_runner + from ..lightx2v.lightx2v.utils.set_config import build_startup_config + + startup_config = build_startup_config(dict(startup_data)) + return _build_runner(startup_config) + + +@contextmanager +def temporary_runner_config(runner, values): + """Apply per-call implementation settings while a request is running. + + LightX2V 0.6 keeps deployment settings in a locked ``LockableDict`` and + exposes request data through ``prepare_request``. A few legacy ComfyUI + controls (SeedVR segmenting/color correction and SwiftVR video encoding) + are still implementation settings, so they need a narrow, serialized + override instead of being passed as unsupported request fields. + """ + values = {key: value for key, value in values.items() if value is not None} + if not values: + yield + return + + config = runner.config + missing = object() + previous = {key: config.get(key, missing) for key in values} + unlock = getattr(config, "temporarily_unlocked", None) + if unlock is None: + raise RuntimeError("LightX2V runner config does not support temporary request settings") + + with unlock(): + config.update(values) + try: + yield + finally: + for key, value in previous.items(): + if value is missing: + config.pop(key, None) + else: + config[key] = value + + +def run_request(runner, request, *, runtime_config=None): + """Validate one modern request and execute it through the runner API.""" + with temporary_runner_config(runner, runtime_config or {}): + input_info = runner.prepare_request(dict(request)) + return runner.run_request(input_info) diff --git a/nodes/seedvr.py b/nodes/seedvr.py index 89f199b..256a3ac 100644 --- a/nodes/seedvr.py +++ b/nodes/seedvr.py @@ -13,7 +13,6 @@ IMAGE tensor (no temp file, no re-encode); the runner's segmenting logic still runs and slices our in-memory tensor. """ -import argparse import gc import logging import math @@ -30,6 +29,7 @@ import folder_paths import torch from comfy.utils import ProgressBar +from ._lightx2v_runner import build_runner, run_request from .file_input import probe_video_file, resolve_input_video_path logger = logging.getLogger(__name__) @@ -209,7 +209,7 @@ def _install_tensor_input_shim(runner, frames_u8, fps): def _probe_video(self, video_path): # noqa: ARG001 total = self._tensor_input.shape[0] - self._set_output_fps(self._tensor_input_fps) + self.set_output_fps(self._tensor_input_fps) return total, self._tensor_input_fps, None def _read_video_segment(self, video_path, start_idx, end_idx): # noqa: ARG001 @@ -330,9 +330,6 @@ class LightX2VSeedVR2Loader: vae_causal_slice_size, vae_memory_limit_gb, ): - from ..lightx2v.lightx2v.infer import init_runner - from ..lightx2v.lightx2v.utils.set_config import set_config - model_dir = _seedvr2_model_dir() if ckpt_name == "None": raise FileNotFoundError(f"No .safetensors checkpoints found in {model_dir}") @@ -376,8 +373,7 @@ class LightX2VSeedVR2Loader: else: config["dit_original_ckpt"] = str(ckpt_path) - formatted = set_config(argparse.Namespace(**config)) - runner = init_runner(formatted) + runner = build_runner(config) logger.info( "[SeedVR2Loader] loaded %s (%s); cpu_offload=%s, tile_vae=%s, tile=%s, overlap=%s, slice=%s, mem_limit=%sGiB", ckpt_name, @@ -467,10 +463,11 @@ class LightX2VSeedVR2Sampler: filename_prefix, color_fix, ): - from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict - runner = model["runner"] + if int(infer_steps) != 1: + raise ValueError("SeedVR2 requires infer_steps=1") + if images.dim() != 4 or images.shape[-1] not in (3, 4): raise ValueError(f"Expected IMAGE [T, H, W, C], got shape {tuple(images.shape)}") # ComfyUI IMAGE: [T, H, W, C] float[0,1] → [T, C, H, W] uint8 (read_video's contract) @@ -496,49 +493,31 @@ class LightX2VSeedVR2Sampler: full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, target_width, target_height) save_path = str(full_path) - # runner.config is a LockableDict (locked after init); set_config uses temporarily_unlocked. - runner.set_config( - { - "sr_ratio": float(sr_ratio), - "target_height": int(target_height), - "target_width": int(target_width), - "target_video_length": int(segment_length), # vestigial for SR; keep aligned with segment_length - "sr_segment_length": int(segment_length), - "sr_overlap": int(segment_overlap), - "infer_steps": int(infer_steps), - "seed": int(seed), - "fps": float(source_fps), - "video_path": "", # truthy sentinel so segmenting logic runs; shim bypasses file I/O - "image_path": "", - "prompt": "", - "negative_prompt": "", - "save_result_path": save_path, - "return_result_tensor": not bool(save_to_output_file), - "color_fix": str(color_fix), - } - ) - - input_info = init_empty_input_info("sr") - update_input_info_from_dict( - input_info, - { - "video_path": "", - "image_path": "", - "prompt": "", - "negative_prompt": "", - "seed": int(seed), - "sr_ratio": float(sr_ratio), - "save_result_path": save_path, - "return_result_tensor": not bool(save_to_output_file), - }, - ) + request = { + "task": "sr", + "video_path": "", + "image_path": "", + "size": [int(target_height), int(target_width)], + "seed": int(seed), + "sr_ratio": float(sr_ratio), + "save_result_path": save_path, + "return_result_tensor": not bool(save_to_output_file), + } + runtime_config = { + "sr_segment_length": int(segment_length), + "sr_overlap": int(segment_overlap), + "fps": float(source_fps), + "color_fix": str(color_fix), + "stream_save_video": bool(save_to_output_file), + } progress = ProgressBar(100) if hasattr(runner, "set_progress_callback"): runner.set_progress_callback(lambda cur, _tot: progress.update_absolute(cur)) try: - result = runner.run_pipeline(input_info) + with _SEEDVR_RUN_LOCK: + result = run_request(runner, request, runtime_config=runtime_config) finally: _clear_tensor_input_shim(runner) torch.cuda.empty_cache() @@ -628,8 +607,6 @@ class LightX2VSeedVR2FileSampler: filename_prefix, color_fix, ): - from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict - input_path = resolve_input_video_path(video_path) source_width, source_height, source_fps = probe_video_file(input_path) effective_fps = source_fps if source_fps > 0 else 16.0 @@ -647,20 +624,25 @@ class LightX2VSeedVR2FileSampler: full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, target_width, target_height) save_path = str(full_path) - input_info = init_empty_input_info("sr") - update_input_info_from_dict( - input_info, - { - "video_path": str(input_path), - "image_path": "", - "prompt": "", - "negative_prompt": "", - "seed": int(seed), - "sr_ratio": float(sr_ratio), - "save_result_path": save_path, - "return_result_tensor": False, - }, - ) + if int(infer_steps) != 1: + raise ValueError("SeedVR2 requires infer_steps=1") + request = { + "task": "sr", + "video_path": str(input_path), + "image_path": "", + "size": [int(target_height), int(target_width)], + "seed": int(seed), + "sr_ratio": float(sr_ratio), + "save_result_path": save_path, + "return_result_tensor": False, + } + runtime_config = { + "sr_segment_length": int(segment_length), + "sr_overlap": int(segment_overlap), + "stream_save_video": True, + "fps": float(effective_fps), + "color_fix": str(color_fix), + } runner = model["runner"] progress = ProgressBar(100) @@ -677,31 +659,9 @@ class LightX2VSeedVR2FileSampler: ) try: with _SEEDVR_RUN_LOCK: - _clear_tensor_input_shim(runner) - runner.set_config( - { - "sr_ratio": float(sr_ratio), - "target_height": int(target_height), - "target_width": int(target_width), - "target_video_length": int(segment_length), - "sr_segment_length": int(segment_length), - "sr_overlap": int(segment_overlap), - "stream_save_video": True, - "infer_steps": int(infer_steps), - "seed": int(seed), - "fps": float(effective_fps), - "video_path": str(input_path), - "image_path": "", - "prompt": "", - "negative_prompt": "", - "save_result_path": save_path, - "return_result_tensor": False, - "color_fix": str(color_fix), - } - ) if hasattr(runner, "set_progress_callback"): runner.set_progress_callback(lambda current, _total: progress.update_absolute(current)) - runner.run_pipeline(input_info) + run_request(runner, request, runtime_config=runtime_config) finally: torch.cuda.empty_cache() gc.collect() diff --git a/nodes/swiftvr.py b/nodes/swiftvr.py index bf3afcf..a394aa1 100644 --- a/nodes/swiftvr.py +++ b/nodes/swiftvr.py @@ -1,6 +1,5 @@ """SwiftVR restoration nodes for ComfyUI.""" -import argparse import gc import logging import tempfile @@ -12,6 +11,7 @@ import folder_paths import torch from comfy.utils import ProgressBar +from ._lightx2v_runner import build_runner, run_request from .file_input import probe_video_file, resolve_input_video_path from .seedvr import _prepare_output_video @@ -189,9 +189,6 @@ class LightX2VSwiftVRLoader: CATEGORY = "LightX2V/SwiftVR" def load(self, model_name, attention_backend, rope_type, clip_length, dit_overlap, reae_frame_batch_size, use_compile): - from ..lightx2v.lightx2v.infer import init_runner - from ..lightx2v.lightx2v.utils.set_config import set_config - model_path = _swiftvr_model_root() / model_name if model_name == "None" or not _is_swiftvr_model(model_path): missing = [str(model_path / relative_path) for relative_path in _REQUIRED_MODEL_FILES if not (model_path / relative_path).is_file()] @@ -218,7 +215,7 @@ class LightX2VSwiftVRLoader: "parallel": False, "use_compile": bool(use_compile), } - runner = init_runner(set_config(argparse.Namespace(**config))) + runner = build_runner(config) logger.info( "[SwiftVRLoader] loaded %s; attention=%s, rope=%s, clip=%s, overlap=%s, reae_batch=%s, compile=%s", model_name, @@ -288,8 +285,6 @@ class LightX2VSwiftVRSampler: ): from lightx2v.models.runners.swiftvr import swiftvr_runner as swiftvr_module - from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict - if images.dim() != 4 or images.shape[-1] not in (3, 4): raise ValueError(f"Expected IMAGE [T, H, W, C], got shape {tuple(images.shape)}") if images.shape[0] < 1: @@ -308,7 +303,7 @@ class LightX2VSwiftVRSampler: target_short_edge, require_even=not is_image, ) - target_shape = [output_height, output_width] + target_size = [output_height, output_width] logger.info( "[SwiftVRSampler] aligned input=%sx%s, target_short_edge=%s, output=%sx%s, scale=%.4f", source_width, @@ -340,18 +335,21 @@ class LightX2VSwiftVRSampler: video_path = "" if is_image else "" return_result_tensor = is_image - input_info = init_empty_input_info("sr") - update_input_info_from_dict( - input_info, - { - "video_path": video_path, - "image_path": image_path, - "sr_ratio": float(sr_ratio), - "target_shape": target_shape, - "save_result_path": save_path, - "return_result_tensor": return_result_tensor, - }, - ) + request = { + "task": "sr", + "video_path": video_path, + "image_path": image_path, + "sr_ratio": float(sr_ratio), + "size": target_size, + "save_result_path": save_path, + "return_result_tensor": return_result_tensor, + } + runtime_config = { + "fps": float(source_fps), + "video_codec": str(video_codec), + "quality": int(quality), + "ffmpeg_preset": str(ffmpeg_preset), + } progress = ProgressBar(100) restored_images = None @@ -364,19 +362,6 @@ class LightX2VSwiftVRSampler: had_instance_image_reader = "read_image_frame" in runner.__dict__ original_instance_image_reader = runner.__dict__.get("read_image_frame") try: - runner.set_config( - { - "fps": float(source_fps), - "video_codec": str(video_codec), - "quality": int(quality), - "ffmpeg_preset": str(ffmpeg_preset), - "video_path": video_path, - "image_path": image_path, - "sr_ratio": float(sr_ratio), - "target_shape": target_shape, - "return_result_tensor": return_result_tensor, - } - ) if hasattr(runner, "set_progress_callback"): runner.set_progress_callback(lambda current, _total: progress.update_absolute(current)) if is_image: @@ -390,7 +375,7 @@ class LightX2VSwiftVRSampler: swiftvr_module.mux_audio_from_video = lambda *_args, **_kwargs: None if not is_image and memory_writer is not None: runner.open_video_writer = types.MethodType(lambda _runner, _path, _fps: memory_writer, runner) - result = runner.run_pipeline(input_info) + result = run_request(runner, request, runtime_config=runtime_config) if is_image: restored_images = result.get("images") if isinstance(result, dict) else result finally: @@ -474,8 +459,6 @@ class LightX2VSwiftVRFileSampler: CATEGORY = "LightX2V/SwiftVR" def sample(self, model, video_path, target_short_edge, filename_prefix, video_codec, quality, ffmpeg_preset): - from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict - input_path = resolve_input_video_path(video_path) raw_width, raw_height, source_fps = probe_video_file(input_path) source_height, source_width = raw_height // 8 * 8, raw_width // 8 * 8 @@ -490,20 +473,21 @@ class LightX2VSwiftVRFileSampler: ) full_path, output_file, output_subfolder = _prepare_output_video(filename_prefix, output_width, output_height) save_path = str(full_path) - target_shape = [output_height, output_width] - - input_info = init_empty_input_info("sr") - update_input_info_from_dict( - input_info, - { - "video_path": str(input_path), - "image_path": "", - "sr_ratio": float(sr_ratio), - "target_shape": target_shape, - "save_result_path": save_path, - "return_result_tensor": False, - }, - ) + request = { + "task": "sr", + "video_path": str(input_path), + "image_path": "", + "sr_ratio": float(sr_ratio), + "size": [output_height, output_width], + "save_result_path": save_path, + "return_result_tensor": False, + } + runtime_config = { + "fps": 0.0, + "video_codec": str(video_codec), + "quality": int(quality), + "ffmpeg_preset": str(ffmpeg_preset), + } runner = model["runner"] progress = ProgressBar(100) @@ -518,22 +502,9 @@ class LightX2VSwiftVRFileSampler: ) try: with _SWIFTVR_RUN_LOCK: - runner.set_config( - { - "fps": 0.0, - "video_codec": str(video_codec), - "quality": int(quality), - "ffmpeg_preset": str(ffmpeg_preset), - "video_path": str(input_path), - "image_path": "", - "sr_ratio": float(sr_ratio), - "target_shape": target_shape, - "return_result_tensor": False, - } - ) if hasattr(runner, "set_progress_callback"): runner.set_progress_callback(lambda current, _total: progress.update_absolute(current)) - runner.run_pipeline(input_info) + run_request(runner, request, runtime_config=runtime_config) finally: torch.cuda.empty_cache() gc.collect()