fix: migrate SeedVR and SwiftVR to LightX2V 0.6 API

This commit is contained in:
gaclove
2026-10-23 00:57:56 +08:00
parent 9cc24e44c3
commit 61a7494e58
3 changed files with 133 additions and 149 deletions
+53
View File
@@ -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)
+45 -85
View File
@@ -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": "<tensor>", # 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": "<tensor>",
"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": "<tensor>",
"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()
+35 -64
View File
@@ -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 "<tensor>"
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()