From 9cc24e44c3264f1e72ca174ad4a0e87cccaaca8a Mon Sep 17 00:00:00 2001 From: gaclove Date: Thu, 22 Oct 2026 23:44:02 +0800 Subject: [PATCH 1/6] fix: adapt wrapper to LightX2V 0.6 runners --- bridge/defaults.py | 1 - bridge/translator/inference.py | 40 ++++++++ config_builder.py | 1 + lightx2v | 2 +- model_utils.py | 18 +++- nodes/inference.py | 169 +++++++++++++++++++++++++++------ 6 files changed, 195 insertions(+), 36 deletions(-) diff --git a/bridge/defaults.py b/bridge/defaults.py index f37ee55..54179c1 100644 --- a/bridge/defaults.py +++ b/bridge/defaults.py @@ -76,5 +76,4 @@ class LightX2VDefaultConfig: "audio_sr": 16000, "talk_objects": None, "boundary_step_index": 2, - "rope_type": "torch", } diff --git a/bridge/translator/inference.py b/bridge/translator/inference.py index 14dc1af..7bd3b1d 100644 --- a/bridge/translator/inference.py +++ b/bridge/translator/inference.py @@ -44,6 +44,39 @@ WRAPPER_TO_LIGHTX2V_FIELDS: Dict[str, str] = { # Attention type fans out to three internal slots in lightx2v. _ATTN_TYPE_SLOTS = ("self_attn_1_type", "cross_attn_1_type", "cross_attn_2_type") +# LightX2V unified the dedicated distilled Wan runners into the regular Wan +# runners in 23fc3d45. Keep old ComfyUI workflows loadable by translating the +# retired model class names to the new model class plus scheduler selector. +_LEGACY_MODEL_CLASS_ALIASES = { + "wan2.1_distill": ("wan2.1", "dmd2"), + "wan2.1_mean_flow_distill": ("wan2.1", "mean_flow"), + "wan2.2_moe_distill": ("wan2.2_moe", "dmd2"), +} + + +def _add_wan22_branch_paths(model_path: str, updates: Dict[str, Any]) -> None: + """Expose Wan2.2's two transformer branches in the runner config. + + Hugging Face model packages commonly keep the branches below + ``distill_models/`` while the unified runner accepts explicit branch + directories. Supplying both path variants is harmless: the runner picks + the original or quantized key according to ``dit_quantized``. + """ + if not model_path: + return + + for branch in ("high_noise_model", "low_noise_model"): + candidates = ( + os.path.join(model_path, branch), + os.path.join(model_path, "distill_models", branch), + ) + branch_path = next((path for path in candidates if os.path.isdir(path)), None) + if branch_path is None: + continue + config_key = branch.removesuffix("_model") + updates.setdefault(f"{config_key}_original_ckpt", branch_path) + updates.setdefault(f"{config_key}_quantized_ckpt", branch_path) + def apply_inference_config(config: Dict[str, Any]) -> Dict[str, Any]: """Translate inference widget values to a partial lightx2v config dict.""" @@ -65,9 +98,16 @@ def apply_inference_config(config: Dict[str, Any]) -> Dict[str, Any]: # Wan2.2 MoE has two CFG scales (high/low noise) and a boundary param. model_cls = config.get("model_cls", "") + legacy_model = _LEGACY_MODEL_CLASS_ALIASES.get(model_cls) + if legacy_model is not None: + model_cls, distill_method = legacy_model + updates["model_cls"] = model_cls + updates["distill_method"] = distill_method + if "wan2.2_moe" in model_cls: updates["boundary"] = 0.9 updates["sample_guide_scale"] = [config.get("cfg_scale"), config.get("cfg_scale2")] + _add_wan22_branch_paths(config.get("model_path", ""), updates) if "wan2.2" in model_cls: updates["use_image_encoder"] = False diff --git a/config_builder.py b/config_builder.py index 16142d2..06db53c 100644 --- a/config_builder.py +++ b/config_builder.py @@ -313,6 +313,7 @@ class ConfigBuilder: ("model_cls", None), ("model_path", None), ("task", None), + ("distill_method", None), ("t5_quantized", False), ("clip_quantized", False), ("lora_configs", None), diff --git a/lightx2v b/lightx2v index 52e86de..94f0f19 160000 --- a/lightx2v +++ b/lightx2v @@ -1 +1 @@ -Subproject commit 52e86de7c252f58cab21ef3f4d533756a10607ac +Subproject commit 94f0f19459e66f61a723db7690f1707f308599ef diff --git a/model_utils.py b/model_utils.py index bd5eb66..c5e3ab9 100644 --- a/model_utils.py +++ b/model_utils.py @@ -30,19 +30,27 @@ def scan_models() -> List[str]: def support_model_cls_list() -> List[str]: + """Return model classes understood by the bundled LightX2V revision. + + The retired ``*_distill`` entries are kept as compatibility aliases for saved + ComfyUI workflows. The current LightX2V runner registry uses the base Wan + class plus ``distill_method`` instead; ``bridge.translator.inference`` + canonicalizes those aliases before the runner is built. + """ return [ "wan2.1", - "wan2.1_distill", "wan2.1_vace", - "cogvideox", "seko_talk", + "seko_talk_ar", "wan2.2_moe", "wan2.2", - "wan2.2_moe_audio", - "wan2.2_audio", - "wan2.2_moe_distill", + "wan2.2_s2v", "qwen_image", "seedvr2", + # Legacy workflow values; normalized before build_runner(). + "wan2.1_distill", + "wan2.1_mean_flow_distill", + "wan2.2_moe_distill", ] diff --git a/nodes/inference.py b/nodes/inference.py index 6a42da3..d079ff8 100644 --- a/nodes/inference.py +++ b/nodes/inference.py @@ -7,9 +7,36 @@ import torch from comfy.utils import ProgressBar from ..config_builder import ConfigBuilder -from ..lightx2v.lightx2v.infer import init_runner -from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info, update_input_info_from_dict -from ..lightx2v.lightx2v.utils.set_config import auto_calc_config, set_args2config + +# LightX2V 0.6 replaced the resident ``init_runner`` entrypoint and the +# ``set_args2config``/``auto_calc_config`` pair with a startup/request split. +# Keep the imports optional so the wrapper can select the API at runtime while +# still supporting the previous submodule revision. +try: + from ..lightx2v.lightx2v.infer import init_runner as _legacy_init_runner +except ImportError: + _legacy_init_runner = None + +try: + from ..lightx2v.lightx2v.utils.input_info import init_empty_input_info as _legacy_init_empty_input_info + from ..lightx2v.lightx2v.utils.input_info import update_input_info_from_dict as _legacy_update_input_info_from_dict +except ImportError: + _legacy_init_empty_input_info = None + _legacy_update_input_info_from_dict = None + +try: + from ..lightx2v.lightx2v.utils.set_config import auto_calc_config as _legacy_auto_calc_config + from ..lightx2v.lightx2v.utils.set_config import set_args2config as _legacy_set_args2config +except ImportError: + _legacy_auto_calc_config = None + _legacy_set_args2config = None + +try: + from ..lightx2v.lightx2v.models.runners.runner_factory import build_runner as _modern_build_runner + from ..lightx2v.lightx2v.utils.set_config import build_startup_config as _modern_build_startup_config +except ImportError: + _modern_build_runner = None + _modern_build_startup_config = None class LightX2VModularInferenceV2: @@ -62,16 +89,33 @@ class LightX2VModularInferenceV2: # never get merged — rs2v_infer then KeyErrors on config["vae_stride"]. if "task" not in config: config["task"] = "rs2v" - # set_config = set_args2config + auto_calc_config. set_args2config strips - # any key that's part of an InputInfo dataclass (target_video_length, - # infer_steps, seed, ...). For CLI runs auto_calc_config recovers them - # by merging --config_json, but we have no external JSON, so - # auto_calc_config's `config["target_video_length"]` access KeyErrors. - # Inject the bridge between set_args2config and auto_calc_config. - target_video_length = config.get("target_video_length", config.get("segment_length", config.get("video_length", 81))) - formatted = set_args2config(config) - formatted["target_video_length"] = target_video_length - formatted = auto_calc_config(formatted) + # LightX2V 0.6 uses the same startup builder as the regular runner; + # older revisions need the set_args2config + auto_calc_config pair. + # The shot runner's load_clip_configs() calls build_startup_config + # again, so passing the modern startup shape here is safe and keeps + # model config/vae metadata loading in LightX2V itself. + if _modern_build_startup_config is not None: + startup_data = dict(config) + startup_data["size"] = [ + int(config.get("target_height", config.get("height", 480))), + int(config.get("target_width", config.get("width", 832))), + ] + startup_data["num_frames"] = int( + config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) + ) + formatted = _modern_build_startup_config(startup_data) + else: + # set_args2config strips any key that's part of an InputInfo + # dataclass (target_video_length, infer_steps, seed, ...). For + # CLI runs auto_calc_config recovers them by merging + # --config_json; our in-memory flow has no external JSON, so + # bridge target_video_length back before auto_calc_config. + target_video_length = config.get("target_video_length", config.get("segment_length", config.get("video_length", 81))) + if _legacy_set_args2config is None or _legacy_auto_calc_config is None: + raise RuntimeError("No compatible LightX2V shot-runner config API found") + formatted = _legacy_set_args2config(config) + formatted["target_video_length"] = target_video_length + formatted = _legacy_auto_calc_config(formatted) main_cfg = { "lightx2v_path": "", "clip_configs": [ @@ -88,6 +132,53 @@ class LightX2VModularInferenceV2: return load_clip_configs(main_cfg) + @staticmethod + def _build_modern_startup_config(config): + """Translate wrapper shape fields into LightX2V 0.6 startup fields.""" + if _modern_build_startup_config is None or _modern_build_runner is None: + raise RuntimeError("The installed LightX2V revision does not provide the modern startup API") + + startup_data = dict(config) + startup_data["size"] = [ + int(config.get("target_height", config.get("height", 480))), + int(config.get("target_width", config.get("width", 832))), + ] + startup_data["num_frames"] = int( + config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) + ) + return _modern_build_startup_config(startup_data) + + @staticmethod + def _build_modern_request(config, runner): + """Build only fields accepted by the current runner's request schema.""" + task = config.get("task") + supported_fields = runner.get_supported_request_fields(task) + request = {} + for field in supported_fields: + if field == "size": + # SekoTalk's fixed-area/min-side modes derive the output size + # from the model's resize policy. The 0.6 request validator + # rejects an explicit size unless the request selected + # fixed_shape; keep the wrapper's target dimensions in startup + # config but do not send them as a per-request override. + if config.get("model_cls", "").startswith("seko") and config.get("resize_mode") != "fixed_shape": + continue + request[field] = [ + int(config.get("target_height", config.get("height", 480))), + int(config.get("target_width", config.get("width", 832))), + ] + elif field == "num_frames": + request[field] = int( + config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) + ) + elif field in config: + request[field] = config[field] + + request["task"] = task + request["return_result_tensor"] = True + request["save_result_path"] = "" + return request + def generate(self, prepared_config): """Run inference with prepared configuration.""" @@ -124,19 +215,22 @@ class LightX2VModularInferenceV2: shot_cfg = self._build_rs2v_shot_config(config) self.__class__._current_runner = ShotRS2VPipeline(shot_cfg) else: - # set_args2config strips InputInfo-dataclass keys (target_video_length, - # infer_steps, ...). CLI flows recover them via --config_json merging - # inside auto_calc_config; our in-memory flow has no external JSON, so - # we bridge target_video_length manually between the two halves so - # auto_calc_config:194's modulo check on s2v/i2v doesn't KeyError. - target_video_length = config.get( - "target_video_length", - config.get("segment_length", config.get("video_length", 81)), - ) - formatted_config = set_args2config(config) - formatted_config["target_video_length"] = target_video_length - formatted_config = auto_calc_config(formatted_config) - self.__class__._current_runner = init_runner(formatted_config) + if _modern_build_runner is not None and _modern_build_startup_config is not None: + startup_config = self._build_modern_startup_config(config) + self.__class__._current_runner = _modern_build_runner(startup_config) + else: + if _legacy_set_args2config is None or _legacy_auto_calc_config is None or _legacy_init_runner is None: + raise RuntimeError("No compatible LightX2V runner API found in the installed LightX2V submodule") + # The legacy API strips InputInfo-dataclass keys. Bridge + # target_video_length back before auto_calc_config. + target_video_length = config.get( + "target_video_length", + config.get("segment_length", config.get("video_length", 81)), + ) + formatted_config = _legacy_set_args2config(config) + formatted_config["target_video_length"] = target_video_length + formatted_config = _legacy_auto_calc_config(formatted_config) + self.__class__._current_runner = _legacy_init_runner(formatted_config) self.__class__._current_config_hash = config_hash progress = ProgressBar(100) @@ -153,10 +247,27 @@ class LightX2VModularInferenceV2: config["save_result_path"] = "" config["negative_prompt"] = config.get("negative_prompt", "") if config.get("task") == "rs2v": - result_dict = current_runner.run_pipeline(config) + # ShotRS2VPipeline delegates request validation to its clip + # runner. Passing the prepared config wholesale worked with + # the old InputInfo path, but LightX2V 0.6 rejects startup + # fields (model paths, quantization flags, etc.) as request + # fields. Filter the request against the modern runner schema + # before handing it to the shot pipeline. + if _modern_build_runner is not None and _modern_build_startup_config is not None: + shot_runner = current_runner.clip_generators["rs2v_clip"] + request_data = self._build_modern_request(config, shot_runner) + result_dict = current_runner.run_pipeline(request_data) + else: + result_dict = current_runner.run_pipeline(config) + elif _modern_build_runner is not None and _modern_build_startup_config is not None: + request_data = self._build_modern_request(config, current_runner) + input_info = current_runner.prepare_request(request_data) + result_dict = current_runner.run_request(input_info) else: - input_data = init_empty_input_info(config.task) - update_input_info_from_dict(input_data, config) + if _legacy_init_empty_input_info is None or _legacy_update_input_info_from_dict is None: + raise RuntimeError("No compatible LightX2V input-info API found in the installed LightX2V submodule") + input_data = _legacy_init_empty_input_info(config.task) + _legacy_update_input_info_from_dict(input_data, config) current_runner.set_config(config) result_dict = current_runner.run_pipeline(input_data) From 61a7494e58d97b0aac7c497d73dc8506c1768aee Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 00:57:56 +0800 Subject: [PATCH 2/6] 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() From a9ee9e9c478361449bba3740a85ce13922cddd71 Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 01:33:35 +0800 Subject: [PATCH 3/6] fix: route VR video workflows through file samplers --- example_workflows/lightx2v_seedvr.json | 153 ++++------ example_workflows/lightx2v_swiftvr_video.json | 277 +++--------------- 2 files changed, 109 insertions(+), 321 deletions(-) diff --git a/example_workflows/lightx2v_seedvr.json b/example_workflows/lightx2v_seedvr.json index 91cba5d..eeba48d 100644 --- a/example_workflows/lightx2v_seedvr.json +++ b/example_workflows/lightx2v_seedvr.json @@ -1,94 +1,65 @@ { - "1": { - "inputs": { - "video": "1954627330480766977_wan2-2.mp4", - "force_rate": 0, - "custom_width": 0, - "custom_height": 0, - "frame_load_cap": 0, - "skip_first_frames": 0, - "select_every_nth": 1, - "format": "AnimateDiff" - }, - "class_type": "VHS_LoadVideo", - "_meta": { - "title": "Load Video (Upload) 🎥🅥🅗🅢" - } + "1": { + "inputs": { + "video": "飞书20260721-124954.mp4" }, - "4": { - "inputs": { - "ckpt_name": "seedvr2_ema_3b_fp8.safetensors", - "precision": "fp8-sgl", - "cpu_offload": false, - "use_tiling_vae": true - }, - "class_type": "LightX2VSeedVR2Loader", - "_meta": { - "title": "LightX2V SeedVR2 Loader" - } - }, - "5": { - "inputs": { - "target_height": 1920, - "target_width": 1080, - "infer_steps": 1, - "segment_length": 81, - "segment_overlap": 1, - "seed": 3816942657, - "source_fps": 16, - "model": [ - "4", - 0 - ], - "images": [ - "1", - 0 - ] - }, - "class_type": "LightX2VSeedVR2Sampler", - "_meta": { - "title": "LightX2V SeedVR2 Sampler" - } - }, - "6": { - "inputs": { - "video_info": [ - "1", - 3 - ] - }, - "class_type": "VHS_VideoInfo", - "_meta": { - "title": "Video Info 🎥🅥🅗🅢" - } - }, - "7": { - "inputs": { - "frame_rate": [ - "6", - 0 - ], - "loop_count": 0, - "filename_prefix": "AnimateDiff", - "format": "video/h264-mp4", - "pix_fmt": "yuv420p", - "crf": 19, - "save_metadata": true, - "trim_to_audio": false, - "pingpong": false, - "save_output": true, - "images": [ - "5", - 0 - ], - "audio": [ - "1", - 2 - ] - }, - "class_type": "VHS_VideoCombine", - "_meta": { - "title": "Video Combine 🎥🅥🅗🅢" - } + "class_type": "LightX2VInputVideoPath", + "_meta": { + "title": "LightX2V Input Video Path (Upload)" } - } \ No newline at end of file + }, + "2": { + "inputs": { + "ckpt_name": "seedvr2_ema_3b_fp8.safetensors", + "precision": "fp8-sgl", + "cpu_offload": false, + "use_tiling_vae": true, + "vae_tile_size": 1024, + "vae_tile_overlap": 64, + "vae_causal_slice_size": 4, + "vae_memory_limit_gb": 0.5 + }, + "class_type": "LightX2VSeedVR2Loader", + "_meta": { + "title": "LightX2V SeedVR2 Loader" + } + }, + "3": { + "inputs": { + "model": [ + "2", + 0 + ], + "video_path": [ + "1", + 0 + ], + "target_width": 1080, + "target_height": 1920, + "infer_steps": 1, + "segment_length": 81, + "segment_overlap": 1, + "seed": 3816942657, + "filename_prefix": "lightx2v_seedvr2/SeedVR2", + "color_fix": "gpu" + }, + "class_type": "LightX2VSeedVR2FileSampler", + "_meta": { + "title": "LightX2V SeedVR2 File Sampler" + } + }, + "4": { + "inputs": { + "filename": [ + "3", + 0 + ], + "validate_exists": true, + "mux_audio": false + }, + "class_type": "LightX2VOutputVideoPreview", + "_meta": { + "title": "LightX2V Output Video Preview" + } + } +} diff --git a/example_workflows/lightx2v_swiftvr_video.json b/example_workflows/lightx2v_swiftvr_video.json index 73ae992..95e8813 100644 --- a/example_workflows/lightx2v_swiftvr_video.json +++ b/example_workflows/lightx2v_swiftvr_video.json @@ -1,8 +1,8 @@ { "id": "2ac26f51-8427-4fd5-a63b-8c0c35716909", "revision": 0, - "last_node_id": 6, - "last_link_id": 10, + "last_node_id": 5, + "last_link_id": 11, "nodes": [ { "id": 2, @@ -41,19 +41,50 @@ false ] }, + { + "id": 5, + "type": "LightX2VInputVideoPath", + "pos": [ + -554.2465629475984, + 172.59098534531842 + ], + "size": [ + 280, + 120 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "video_path", + "type": "STRING", + "links": [ + 11 + ] + } + ], + "properties": { + "Node name for S&R": "LightX2VInputVideoPath" + }, + "widgets_values": [ + "飞书20260721-124954.mp4" + ] + }, { "id": 3, - "type": "LightX2VSwiftVRSampler", + "type": "LightX2VSwiftVRFileSampler", "pos": [ 410.84880341840596, 119.34257282956952 ], "size": [ 337.3125, - 320 + 280 ], "flags": {}, - "order": 3, + "order": 2, "mode": 0, "inputs": [ { @@ -62,25 +93,12 @@ "link": 4 }, { - "name": "images", - "type": "IMAGE", - "link": 7 - }, - { - "name": "source_fps", - "type": "FLOAT", - "widget": { - "name": "source_fps" - }, - "link": 9 + "name": "video_path", + "type": "STRING", + "link": 11 } ], "outputs": [ - { - "name": "images", - "type": "IMAGE", - "links": null - }, { "name": "filename", "type": "STRING", @@ -90,12 +108,10 @@ } ], "properties": { - "Node name for S&R": "LightX2VSwiftVRSampler" + "Node name for S&R": "LightX2VSwiftVRFileSampler" }, "widgets_values": [ 1080, - 16, - true, "lightx2v_swiftvr_api/SwiftVR", "libx265", 60, @@ -114,19 +130,13 @@ 703.328125 ], "flags": {}, - "order": 4, + "order": 3, "mode": 0, "inputs": [ { "name": "filename", "type": "STRING", "link": 6 - }, - { - "name": "audio", - "shape": 7, - "type": "AUDIO", - "link": 10 } ], "outputs": [ @@ -141,173 +151,8 @@ }, "widgets_values": [ true, - true + false ] - }, - { - "id": 5, - "type": "VHS_LoadVideo", - "pos": [ - -554.2465629475984, - 172.59098534531842 - ], - "size": [ - 260.421875, - 761.6875 - ], - "flags": {}, - "order": 1, - "mode": 0, - "inputs": [ - { - "name": "meta_batch", - "shape": 7, - "type": "VHS_BatchManager", - "link": null - }, - { - "name": "vae", - "shape": 7, - "type": "VAE", - "link": null - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 7 - ] - }, - { - "name": "frame_count", - "type": "INT", - "links": null - }, - { - "name": "audio", - "type": "AUDIO", - "links": [ - 10 - ] - }, - { - "name": "video_info", - "type": "VHS_VIDEOINFO", - "links": [ - 8 - ] - } - ], - "properties": { - "Node name for S&R": "VHS_LoadVideo" - }, - "widgets_values": { - "video": "飞书20260721-124954.mp4", - "force_rate": 0, - "custom_width": 0, - "custom_height": 0, - "frame_load_cap": 0, - "skip_first_frames": 0, - "select_every_nth": 1, - "format": "AnimateDiff", - "videopreview": { - "hidden": false, - "paused": false, - "params": { - "filename": "飞书20260721-124954.mp4", - "type": "input", - "format": "video/mp4", - "force_rate": 0, - "custom_width": 0, - "custom_height": 0, - "frame_load_cap": 0, - "skip_first_frames": 0, - "select_every_nth": 1 - } - } - } - }, - { - "id": 6, - "type": "VHS_VideoInfo", - "pos": [ - -245.90446161258, - 309.43756146966683 - ], - "size": [ - 233.453125, - 288 - ], - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [ - { - "name": "video_info", - "type": "VHS_VIDEOINFO", - "link": 8 - } - ], - "outputs": [ - { - "name": "source_fps🟨", - "type": "FLOAT", - "links": [ - 9 - ] - }, - { - "name": "source_frame_count🟨", - "type": "INT", - "links": null - }, - { - "name": "source_duration🟨", - "type": "FLOAT", - "links": null - }, - { - "name": "source_width🟨", - "type": "INT", - "links": null - }, - { - "name": "source_height🟨", - "type": "INT", - "links": null - }, - { - "name": "loaded_fps🟦", - "type": "FLOAT", - "links": null - }, - { - "name": "loaded_frame_count🟦", - "type": "INT", - "links": null - }, - { - "name": "loaded_duration🟦", - "type": "FLOAT", - "links": null - }, - { - "name": "loaded_width🟦", - "type": "INT", - "links": null - }, - { - "name": "loaded_height🟦", - "type": "INT", - "links": null - } - ], - "properties": { - "Node name for S&R": "VHS_VideoInfo" - }, - "widgets_values": {} } ], "links": [ @@ -322,42 +167,18 @@ [ 6, 3, - 1, + 0, 4, 0, "STRING" ], [ - 7, + 11, 5, 0, 3, 1, - "IMAGE" - ], - [ - 8, - 5, - 3, - 6, - 0, - "VHS_VIDEOINFO" - ], - [ - 9, - 6, - 0, - 3, - 2, - "FLOAT" - ], - [ - 10, - 5, - 2, - 4, - 1, - "AUDIO" + "STRING" ] ], "groups": [], @@ -370,11 +191,7 @@ 389.36680475087843 ] }, - "frontendVersion": "1.45.15", - "VHS_latentpreview": false, - "VHS_latentpreviewrate": 0, - "VHS_MetadataImage": true, - "VHS_KeepIntermediate": true + "frontendVersion": "1.45.15" }, "version": 0.4 -} \ No newline at end of file +} From ecb81530f7f5c3599655ea690017ef7795b7724b Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 03:07:52 +0800 Subject: [PATCH 4/6] fix: update SekoTalk workflows for LightX2V 0.6 --- example_workflows/lightx2v_seko_talk.json | 93 ++-- .../lightx2v_seko_talk_multi_talk.json | 433 ++++-------------- 2 files changed, 146 insertions(+), 380 deletions(-) diff --git a/example_workflows/lightx2v_seko_talk.json b/example_workflows/lightx2v_seko_talk.json index 10277db..ca7a154 100644 --- a/example_workflows/lightx2v_seko_talk.json +++ b/example_workflows/lightx2v_seko_talk.json @@ -1,7 +1,7 @@ { "11": { "inputs": { - "audio": "12秒.mp3", + "audio": "audio_full_female_10s.wav", "start_time": 0, "duration": 0 }, @@ -25,6 +25,14 @@ "audio": [ "11", 0 + ], + "memory_config": [ + "16", + 0 + ], + "quantization_config": [ + "17", + 0 ] }, "class_type": "LightX2VConfigCombinerV2", @@ -35,7 +43,7 @@ "14": { "inputs": { "model_cls": "seko_talk", - "model_name": "SekoTalk-v2.7_beta2-bf16-step4_temp", + "model_name": "SekoTalk-v2.7-fp8-step4-xt-490", "task": "rs2v", "infer_steps": 4, "seed": 4221706066, @@ -70,23 +78,54 @@ "title": "LightX2V Modular Inference V2" } }, + "16": { + "inputs": { + "enable_rotary_chunk": false, + "rotary_chunk_size": 100, + "clean_cuda_cache": true, + "cpu_offload": true, + "offload_granularity": "block", + "offload_ratio": 1.0, + "t5_cpu_offload": true, + "t5_offload_granularity": "model", + "audio_encoder_cpu_offload": true, + "audio_adapter_cpu_offload": true, + "vae_cpu_offload": true, + "use_tiling_vae": true, + "lazy_load": false, + "unload_after_inference": false + }, + "class_type": "LightX2VMemoryOptimization", + "_meta": { + "title": "LightX2V Memory Optimization" + } + }, + "17": { + "inputs": { + "dit_quant_scheme": "fp8-sgl", + "t5_quant_scheme": "fp8-sgl", + "clip_quant_scheme": "fp8-sgl", + "adapter_quant_scheme": "fp8-sgl" + }, + "class_type": "LightX2VQuantization", + "_meta": { + "title": "LightX2V Quantization" + } + }, "18": { "inputs": { - "frame_rate": [ - "22", - 0 - ], + "frame_rate": 16, "loop_count": 0, - "filename_prefix": "vigen-15ebb023", + "filename_prefix": "lightx2v_sekotalk/single", "format": "video/h264-mp4", "pix_fmt": "yuv420p", - "crf": 23, + "crf": 19, "save_metadata": false, "trim_to_audio": false, "pingpong": false, - "save_output": false, + "save_output": true, "images": [ - "21", + "15", 0 ], "audio": [ @@ -101,41 +140,11 @@ }, "20": { "inputs": { - "image": "00000.jpg" + "image": "codex_image_20260922_135831_1323a530_B.jpg" }, "class_type": "LoadImage", "_meta": { "title": "Load Image" } - }, - "21": { - "inputs": { - "source_fps": 16, - "target_fps": [ - "22", - 0 - ], - "scale": 1, - "model_name": "flownet.pkl", - "batch_size": 8, - "use_fp16": true, - "images": [ - "15", - 0 - ] - }, - "class_type": "RIFEInterpolation", - "_meta": { - "title": "RIFE Frame Interpolation" - } - }, - "22": { - "inputs": { - "value": 25.000000000000007 - }, - "class_type": "FloatConstant", - "_meta": { - "title": "target fps" - } } -} \ No newline at end of file +} diff --git a/example_workflows/lightx2v_seko_talk_multi_talk.json b/example_workflows/lightx2v_seko_talk_multi_talk.json index 8532ab3..9d81b96 100644 --- a/example_workflows/lightx2v_seko_talk_multi_talk.json +++ b/example_workflows/lightx2v_seko_talk_multi_talk.json @@ -1,8 +1,8 @@ { "id": "00000000-0000-0000-0000-000000000000", "revision": 0, - "last_node_id": 50, - "last_link_id": 49, + "last_node_id": 51, + "last_link_id": 50, "nodes": [ { "id": 10, @@ -32,16 +32,15 @@ "Node name for S&R": "LightX2VQuantization" }, "widgets_values": [ - "sgl", - "fp8", - "fp8", - "fp16", - "fp8" + "fp8-sgl", + "fp8-sgl", + "fp8-sgl", + "fp8-sgl" ] }, { "id": 11, - "type": "LightX2VConfigCombiner", + "type": "LightX2VConfigCombinerV3", "pos": [ -1207.53271484375, 111.63562774658203 @@ -51,7 +50,7 @@ 126 ], "flags": {}, - "order": 14, + "order": 16, "mode": 0, "inputs": [ { @@ -75,7 +74,7 @@ "name": "memory_config", "shape": 7, "type": "MEMORY_CONFIG", - "link": null + "link": 50 }, { "name": "lora_chain", @@ -88,41 +87,6 @@ "shape": 7, "type": "TALK_OBJECTS_CONFIG", "link": 49 - } - ], - "outputs": [ - { - "name": "combined_config", - "type": "COMBINED_CONFIG", - "links": [ - 13 - ] - } - ], - "properties": { - "Node name for S&R": "LightX2VConfigCombiner" - }, - "widgets_values": [] - }, - { - "id": 12, - "type": "LightX2VModularInference", - "pos": [ - -818.747802734375, - 89.00883483886719 - ], - "size": [ - 400, - 200 - ], - "flags": {}, - "order": 16, - "mode": 0, - "inputs": [ - { - "name": "combined_config", - "type": "COMBINED_CONFIG", - "link": 13 }, { "name": "image", @@ -134,7 +98,42 @@ "name": "audio", "shape": 7, "type": "AUDIO", - "link": 15 + "link": null + } + ], + "outputs": [ + { + "name": "prepared_config", + "type": "PREPARED_CONFIG", + "links": [ + 13 + ] + } + ], + "properties": { + "Node name for S&R": "LightX2VConfigCombinerV3" + }, + "widgets_values": [] + }, + { + "id": 12, + "type": "LightX2VModularInferenceV2", + "pos": [ + -818.747802734375, + 89.00883483886719 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "prepared_config", + "type": "PREPARED_CONFIG", + "link": 13 } ], "outputs": [ @@ -154,7 +153,7 @@ } ], "properties": { - "Node name for S&R": "LightX2VModularInference" + "Node name for S&R": "LightX2VModularInferenceV2" }, "widgets_values": [ "The video feature the person is talking.", @@ -195,44 +194,10 @@ "Node name for S&R": "LoadImage" }, "widgets_values": [ - "input (1).png", + "codex_two_person_input.png", "image" ] }, - { - "id": 20, - "type": "LoadAudio", - "pos": [ - -1618.51171875, - 526.3804931640625 - ], - "size": [ - 274.080078125, - 136 - ], - "flags": {}, - "order": 10, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "AUDIO", - "type": "AUDIO", - "links": [ - 15 - ] - } - ], - "title": "加载音频", - "properties": { - "Node name for S&R": "LoadAudio" - }, - "widgets_values": [ - "15.wav", - null, - null - ] - }, { "id": 21, "type": "VHS_VideoCombine", @@ -337,8 +302,8 @@ }, "widgets_values": [ "seko_talk", - "Wan2.1-R2V0909-Audio-14B-720P-fp8", - "i2v", + "SekoTalk-v2.7-fp8-step4-xt-490", + "rs2v", 4, 584734195, "randomize", @@ -357,42 +322,6 @@ false ] }, - { - "id": 34, - "type": "easy showAnything", - "pos": [ - -508.2369079589844, - -330.85296630859375 - ], - "size": [ - 511.8559875488281, - 291.96728515625 - ], - "flags": {}, - "order": 17, - "mode": 0, - "inputs": [ - { - "name": "anything", - "shape": 7, - "type": "*", - "link": 42 - } - ], - "outputs": [ - { - "name": "output", - "type": "*", - "links": [] - } - ], - "properties": { - "Node name for S&R": "easy showAnything" - }, - "widgets_values": [ - "TalkObjectsConfig(talk_objects=[{'name': 'person_1', 'audio': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_audio.wav', 'mask': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_mask.png', 'source_type': 'file'}, {'name': 'person_1', 'audio': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_audio.wav', 'mask': '/mnt/aigc/users/gaopeng1/ComfyUI/input/person_1_mask.png', 'source_type': 'file'}])" - ] - }, { "id": 36, "type": "LoadAudio", @@ -423,7 +352,7 @@ "Node name for S&R": "LoadAudio" }, "widgets_values": [ - "p2.mp3", + "audio_full_female_10s.wav", null, null ] @@ -458,8 +387,8 @@ "Node name for S&R": "LoadImageMask" }, "widgets_values": [ - "input_p2_mask.png", - "red", + "codex_two_person_input.png", + "blue", "image" ] }, @@ -493,77 +422,7 @@ "Node name for S&R": "LoadImageMask" }, "widgets_values": [ - "input_p1_mask.png", - "red", - "image" - ] - }, - { - "id": 41, - "type": "LoadAudio", - "pos": [ - -1468.0582275390625, - -386.4920654296875 - ], - "size": [ - 274.080078125, - 136 - ], - "flags": { - "collapsed": true - }, - "order": 4, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "AUDIO", - "type": "AUDIO", - "links": [ - 44 - ] - } - ], - "properties": { - "Node name for S&R": "LoadAudio" - }, - "widgets_values": [ - "15.wav", - null, - null - ] - }, - { - "id": 42, - "type": "LoadImageMask", - "pos": [ - -1477.0850830078125, - -332.2462463378906 - ], - "size": [ - 274.080078125, - 318 - ], - "flags": { - "collapsed": false - }, - "order": 3, - "mode": 0, - "inputs": [], - "outputs": [ - { - "name": "MASK", - "type": "MASK", - "links": [ - 45 - ] - } - ], - "properties": { - "Node name for S&R": "LoadImageMask" - }, - "widgets_values": [ - "input_p3_mask.png", + "codex_two_person_input.png", "red", "image" ] @@ -649,8 +508,7 @@ "name": "talk_objects_config", "type": "TALK_OBJECTS_CONFIG", "links": [ - 42, - 48 + 49 ] } ], @@ -744,50 +602,7 @@ "Node name for S&R": "LightX2VTalkObjectInput" }, "widgets_values": [ - "person_1", - true - ] - }, - { - "id": 46, - "type": "LightX2VTalkObjectInput", - "pos": [ - -937.0840454101562, - -414.41925048828125 - ], - "size": [ - 317.0162048339844, - 102 - ], - "flags": {}, - "order": 12, - "mode": 0, - "inputs": [ - { - "name": "audio", - "shape": 7, - "type": "AUDIO", - "link": 44 - }, - { - "name": "mask", - "shape": 7, - "type": "MASK", - "link": 45 - } - ], - "outputs": [ - { - "name": "talk_object", - "type": "TALK_OBJECT", - "links": [] - } - ], - "properties": { - "Node name for S&R": "LightX2VTalkObjectInput" - }, - "widgets_values": [ - "person_1", + "person_2", true ] }, @@ -821,59 +636,21 @@ "Node name for S&R": "LoadAudio" }, "widgets_values": [ - "p1.mp3", + "male_10s.wav", null, "" ] }, { - "id": 48, - "type": "SetNode", + "id": 51, + "type": "LightX2VMemoryOptimization", "pos": [ - -199.871337890625, - -578.7578735351562 + -1750, + 420 ], "size": [ - 210, - 60 - ], - "flags": {}, - "order": 18, - "mode": 0, - "inputs": [ - { - "name": "TALK_OBJECTS_CONFIG", - "type": "TALK_OBJECTS_CONFIG", - "link": 48 - } - ], - "outputs": [ - { - "name": "*", - "type": "*", - "links": null - } - ], - "title": "Set_talk_objects", - "properties": { - "previousName": "talk_objects" - }, - "widgets_values": [ - "talk_objects" - ], - "color": "#232", - "bgcolor": "#353" - }, - { - "id": 50, - "type": "GetNode", - "pos": [ - -1453.525390625, - 249.66848754882812 - ], - "size": [ - 210, - 60 + 290, + 420 ], "flags": {}, "order": 7, @@ -881,20 +658,32 @@ "inputs": [], "outputs": [ { - "name": "TALK_OBJECTS_CONFIG", - "type": "TALK_OBJECTS_CONFIG", + "name": "memory_config", + "type": "MEMORY_CONFIG", "links": [ - 49 + 50 ] } ], - "title": "Get_talk_objects", - "properties": {}, + "properties": { + "Node name for S&R": "LightX2VMemoryOptimization" + }, "widgets_values": [ - "talk_objects" - ], - "color": "#232", - "bgcolor": "#353" + false, + 100, + true, + true, + "block", + 1.0, + true, + "model", + true, + true, + true, + true, + false, + false + ] } ], "links": [ @@ -920,24 +709,16 @@ 0, 12, 0, - "COMBINED_CONFIG" + "PREPARED_CONFIG" ], [ 14, 19, 0, - 12, - 1, + 11, + 6, "IMAGE" ], - [ - 15, - 20, - 0, - 12, - 2, - "AUDIO" - ], [ 18, 12, @@ -994,30 +775,6 @@ 1, "TALK_OBJECT" ], - [ - 42, - 43, - 0, - 34, - 0, - "*" - ], - [ - 44, - 41, - 0, - 46, - 0, - "AUDIO" - ], - [ - 45, - 42, - 0, - 46, - 1, - "MASK" - ], [ 47, 47, @@ -1026,21 +783,21 @@ 0, "AUDIO" ], - [ - 48, - 43, - 0, - 48, - 0, - "*" - ], [ 49, - 50, + 43, 0, 11, 5, "TALK_OBJECTS_CONFIG" + ], + [ + 50, + 51, + 0, + 11, + 3, + "MEMORY_CONFIG" ] ], "groups": [ From c13737d6d049a30bcdd93079d7710744329274bd Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 14:26:36 +0800 Subject: [PATCH 5/6] fix: finalize LightX2V 0.6 wrapper migration --- .gitignore | 2 +- .../lightx2v_seko_talk_static.json | 150 ++++++++++++++++++ example_workflows/lightx2v_swiftvr_image.json | 2 +- nodes/_audio_shim.py | 15 +- nodes/_image_shim.py | 113 +++++++++++++ nodes/combiner.py | 19 ++- nodes/config.py | 6 +- nodes/inference.py | 30 ++-- nodes/seedvr.py | 2 +- nodes/swiftvr.py | 8 +- 10 files changed, 315 insertions(+), 32 deletions(-) create mode 100644 example_workflows/lightx2v_seko_talk_static.json create mode 100644 nodes/_image_shim.py diff --git a/.gitignore b/.gitignore index 0bbb58e..c44cdb7 100644 --- a/.gitignore +++ b/.gitignore @@ -346,4 +346,4 @@ Temporary Items CLAUDE.md AGENTS.md .gitnexus/ -.claude/ \ No newline at end of file +.claude/ diff --git a/example_workflows/lightx2v_seko_talk_static.json b/example_workflows/lightx2v_seko_talk_static.json new file mode 100644 index 0000000..02f254f --- /dev/null +++ b/example_workflows/lightx2v_seko_talk_static.json @@ -0,0 +1,150 @@ +{ + "11": { + "inputs": { + "audio": "audio_full_female_10s.wav", + "start_time": 0, + "duration": 0 + }, + "class_type": "VHS_LoadAudioUpload", + "_meta": { + "title": "Load Audio (Upload)🎥🅥🅗🅢" + } + }, + "13": { + "inputs": { + "prompt": "A person speaks naturally to the camera, with clear lip movement and subtle facial expressions.", + "negative_prompt": "", + "inference_config": [ + "14", + 0 + ], + "image": [ + "20", + 0 + ], + "audio": [ + "11", + 0 + ], + "memory_config": [ + "16", + 0 + ], + "quantization_config": [ + "17", + 0 + ] + }, + "class_type": "LightX2VConfigCombinerV2", + "_meta": { + "title": "LightX2V Config Combiner V2" + } + }, + "14": { + "inputs": { + "model_cls": "seko_talk", + "model_name": "Wan2.1-R2V0909-Audio-14B-720P-fp8", + "task": "s2v", + "infer_steps": 4, + "seed": 4221706066, + "cfg_scale": 1, + "cfg_scale2": 1, + "sample_shift": 5, + "height": 1280, + "width": 720, + "duration": 5, + "attention_type": "sage_attn2", + "denoising_steps": "", + "resize_mode": "adaptive", + "fixed_area": "720p", + "segment_length": 81, + "prev_frame_length": 5, + "use_tiny_vae": false + }, + "class_type": "LightX2VInferenceConfig", + "_meta": { + "title": "LightX2V Inference Config" + } + }, + "15": { + "inputs": { + "prepared_config": [ + "13", + 0 + ] + }, + "class_type": "LightX2VModularInferenceV2", + "_meta": { + "title": "LightX2V Modular Inference V2" + } + }, + "16": { + "inputs": { + "enable_rotary_chunk": false, + "rotary_chunk_size": 100, + "clean_cuda_cache": true, + "cpu_offload": true, + "offload_granularity": "block", + "offload_ratio": 1.0, + "t5_cpu_offload": true, + "t5_offload_granularity": "model", + "audio_encoder_cpu_offload": true, + "audio_adapter_cpu_offload": true, + "vae_cpu_offload": true, + "use_tiling_vae": true, + "lazy_load": false, + "unload_after_inference": false + }, + "class_type": "LightX2VMemoryOptimization", + "_meta": { + "title": "LightX2V Memory Optimization" + } + }, + "17": { + "inputs": { + "dit_quant_scheme": "fp8-sgl", + "t5_quant_scheme": "fp8-sgl", + "clip_quant_scheme": "fp8-sgl", + "adapter_quant_scheme": "fp8-sgl" + }, + "class_type": "LightX2VQuantization", + "_meta": { + "title": "LightX2V Quantization" + } + }, + "18": { + "inputs": { + "frame_rate": 16, + "loop_count": 0, + "filename_prefix": "lightx2v_sekotalk/static", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": false, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "images": [ + "15", + 0 + ], + "audio": [ + "15", + 1 + ] + }, + "class_type": "VHS_VideoCombine", + "_meta": { + "title": "Video Combine 🎥🅥🅗🅢" + } + }, + "20": { + "inputs": { + "image": "codex_image_20260922_135831_1323a530_B.jpg" + }, + "class_type": "LoadImage", + "_meta": { + "title": "Load Image" + } + } +} diff --git a/example_workflows/lightx2v_swiftvr_image.json b/example_workflows/lightx2v_swiftvr_image.json index b627b4d..5bec0cf 100644 --- a/example_workflows/lightx2v_swiftvr_image.json +++ b/example_workflows/lightx2v_swiftvr_image.json @@ -202,4 +202,4 @@ "VHS_KeepIntermediate": true }, "version": 0.4 -} \ No newline at end of file +} diff --git a/nodes/_audio_shim.py b/nodes/_audio_shim.py index 8db60fd..aa073fc 100644 --- a/nodes/_audio_shim.py +++ b/nodes/_audio_shim.py @@ -41,6 +41,17 @@ _PATCH_SITES = ( ) +class _AudioSentinel(str): + """A path-shaped token that releases its registry lease when discarded.""" + + def __del__(self): + try: + release(str(self)) + except Exception: + # Destructors can run during interpreter shutdown. + pass + + def register(waveform: torch.Tensor, sample_rate: int) -> str: """Register a [C, T] float32 waveform; return a sentinel "path" for it.""" if waveform.dim() != 2: @@ -50,12 +61,12 @@ def register(waveform: torch.Tensor, sample_rate: int) -> str: stashed = waveform.detach().to(torch.float32).cpu().contiguous() with _LOCK: _REGISTRY[sentinel] = (stashed, int(sample_rate)) - return sentinel + return _AudioSentinel(sentinel) def release(sentinel: str) -> None: with _LOCK: - _REGISTRY.pop(sentinel, None) + _REGISTRY.pop(str(sentinel), None) def is_sentinel(path) -> bool: diff --git a/nodes/_image_shim.py b/nodes/_image_shim.py new file mode 100644 index 0000000..25dd7fe --- /dev/null +++ b/nodes/_image_shim.py @@ -0,0 +1,113 @@ +"""Keep ComfyUI IMAGE inputs in memory while LightX2V reads them. + +LightX2V's runner API names the request field ``image_path`` and accepts a +PIL image at the final read boundary. The wrapper used to serialize the first +ComfyUI frame to a temporary PNG only to have the runner decode it again. This +shim keeps the path-shaped request contract while replacing that disk +round-trip with a short-lived in-memory PIL image. +""" + +from __future__ import annotations + +import logging +import threading +import types +import uuid +from typing import Dict + +import torch +from PIL import Image + +SENTINEL_PREFIX = " str: + """Register the first ComfyUI IMAGE frame and return a path-shaped token.""" + if not torch.is_tensor(images) or images.ndim != 4: + raise ValueError(f"IMAGE must be a [B, H, W, C] tensor; got {type(images).__name__} {getattr(images, 'shape', '')}") + if images.shape[-1] not in (1, 3, 4): + raise ValueError(f"IMAGE must have 1, 3, or 4 channels; got shape {tuple(images.shape)}") + + frame = images[0].detach().cpu().float().clamp(0.0, 1.0) + frame_u8 = (frame.mul(255.0).round()).to(torch.uint8).numpy() + if frame_u8.shape[-1] == 1: + frame_u8 = frame_u8[..., 0] + elif frame_u8.shape[-1] == 4: + frame_u8 = frame_u8[..., :3] + + token = f"{SENTINEL_PREFIX}{uuid.uuid4().hex}{SENTINEL_SUFFIX}" + image = Image.fromarray(frame_u8) + if image.mode != "RGB": + image = image.convert("RGB") + with _LOCK: + _REGISTRY[token] = image + return _ImageSentinel(token) + + +def release(sentinel: str | None) -> None: + if not is_sentinel(sentinel): + return + with _LOCK: + _REGISTRY.pop(str(sentinel), None) + + +def is_sentinel(path) -> bool: + return isinstance(path, str) and path.startswith(SENTINEL_PREFIX) + + +def _lookup(sentinel: str) -> Image.Image: + with _LOCK: + image = _REGISTRY.get(sentinel) + if image is None: + raise FileNotFoundError(f"Stale LightX2V in-memory image sentinel: {sentinel}") + return image + + +def install(runner) -> bool: + """Patch one runner's image read boundary; return whether it was patched.""" + if runner is None or not hasattr(runner, "read_image_input"): + return False + if hasattr(runner, "_lightx2v_original_read_image_input"): + return True + + original = runner.read_image_input + + def _read_image_input(self, image_path): + if is_sentinel(image_path): + image_path = _lookup(image_path) + return original(image_path) + + runner._lightx2v_original_read_image_input = original + runner.read_image_input = types.MethodType(_read_image_input, runner) + logging.info("_image_shim installed on %s", type(runner).__name__) + return True + + +def install_on_runner_tree(runner) -> int: + """Patch a regular runner or every clip runner inside an rs2v pipeline.""" + targets = [runner] + clip_generators = getattr(runner, "clip_generators", None) + if isinstance(clip_generators, dict): + targets.extend(clip_generators.values()) + return sum(1 for target in targets if install(target)) diff --git a/nodes/combiner.py b/nodes/combiner.py index f1b64a6..2091c11 100644 --- a/nodes/combiner.py +++ b/nodes/combiner.py @@ -168,19 +168,24 @@ class _BaseConfigCombiner: if image is None: raise ValueError("i2v or s2v or rs2v task requires input image") - image_np = (image[0].cpu().numpy() * 255).astype(np.uint8) - pil_image = Image.fromarray(image_np) - temp_path = self.temp_manager.create_temp_file(suffix=".png") - pil_image.save(temp_path) - config.image_path = temp_path - logging.info(f"Image saved to {temp_path}") + frame_count = int(image.shape[0]) if hasattr(image, "shape") and image.ndim > 0 else 0 + if frame_count > 1: + logging.warning( + "LightX2V uses only the first IMAGE frame as the reference; received %s frames", + frame_count, + ) + + from ._image_shim import register + + config.image_path = register(image) + logging.info("Routed ComfyUI IMAGE through the in-memory shim") def _save_single_audio_if_needed(self, config, audio): # Route ComfyUI AUDIO straight into the runner via the in-memory shim # (no WAV temp file, no soundfile round-trip). The shim only intercepts # this single-AUDIO path; V3 multi-talker padding still produces real # files since its inputs are external paths/URLs, not ComfyUI tensors. - if audio is None or not hasattr(config, "model_cls") or "seko" not in config.model_cls: + if audio is None or config.task not in {"s2v", "rs2v"}: return from ._audio_shim import comfyui_audio_to_loader_pair, install, register diff --git a/nodes/config.py b/nodes/config.py index 4d85ef3..5cbecb7 100644 --- a/nodes/config.py +++ b/nodes/config.py @@ -42,7 +42,7 @@ class LightX2VInferenceConfig: ["t2v", "i2v", "s2v", "rs2v"], { "default": "i2v", - "tooltip": "Task type: text-to-video or image-to-video or reference_image and audio to video (shot)", + "tooltip": "Task: t2v text-to-video, i2v image-to-video, s2v image+audio-to-video, or rs2v reference-shot image+audio-to-video", }, ), "infer_steps": ( @@ -136,7 +136,7 @@ class LightX2VInferenceConfig: ], { "default": "adaptive", - "tooltip": "Adaptive resize input image to target aspect ratio", + "tooltip": "Resize policy: adaptive preserves the target area, fixed_shape uses the exact target size, and fixed-area/min-side modes preserve aspect ratio", }, ), "fixed_area": ( @@ -161,7 +161,7 @@ class LightX2VInferenceConfig: "default": 5, "min": 0, "max": 16, - "tooltip": "Previous frame overlap for sekotalk models", + "tooltip": "Previous-frame overlap for rs2v continuation; s2v uses the model's own initial reference and usually ignores this value", }, ), "use_tiny_vae": ( diff --git a/nodes/inference.py b/nodes/inference.py index d079ff8..633d4bd 100644 --- a/nodes/inference.py +++ b/nodes/inference.py @@ -100,9 +100,7 @@ class LightX2VModularInferenceV2: int(config.get("target_height", config.get("height", 480))), int(config.get("target_width", config.get("width", 832))), ] - startup_data["num_frames"] = int( - config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) - ) + startup_data["num_frames"] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81)))) formatted = _modern_build_startup_config(startup_data) else: # set_args2config strips any key that's part of an InputInfo @@ -143,9 +141,7 @@ class LightX2VModularInferenceV2: int(config.get("target_height", config.get("height", 480))), int(config.get("target_width", config.get("width", 832))), ] - startup_data["num_frames"] = int( - config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) - ) + startup_data["num_frames"] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81)))) return _modern_build_startup_config(startup_data) @staticmethod @@ -168,9 +164,7 @@ class LightX2VModularInferenceV2: int(config.get("target_width", config.get("width", 832))), ] elif field == "num_frames": - request[field] = int( - config.get("target_video_length", config.get("video_length", config.get("num_frames", 81))) - ) + request[field] = int(config.get("target_video_length", config.get("video_length", config.get("num_frames", 81)))) elif field in config: request[field] = config[field] @@ -183,6 +177,14 @@ class LightX2VModularInferenceV2: """Run inference with prepared configuration.""" config = prepared_config + from ._image_shim import install_on_runner_tree as _install_image_shim + from ._image_shim import is_sentinel as _is_image_sentinel + from ._image_shim import release as _release_image_sentinel + + _image_sentinel = config.get("image_path") if isinstance(config, dict) else getattr(config, "image_path", None) + if not _is_image_sentinel(_image_sentinel): + _image_sentinel = None + # Combiner may have stashed the input AUDIO in the in-memory shim and # set audio_path to a sentinel — release it on exit so the tensor isn't # retained across runs (one ComfyUI graph tick = one sentinel). @@ -199,7 +201,7 @@ class LightX2VModularInferenceV2: current_runner = getattr(self.__class__, "_current_runner", None) current_config_hash = getattr(self.__class__, "_current_config_hash", None) - needs_reinit = current_runner is None or current_config_hash != config_hash or getattr(config, "lazy_load", False) + needs_reinit = current_runner is None or current_config_hash != config_hash or config.get("lazy_load", False) logging.info(f"Needs reinit: {needs_reinit}, old config hash: {current_config_hash}, new config hash: {config_hash}") if needs_reinit: @@ -240,6 +242,10 @@ class LightX2VModularInferenceV2: current_runner = self.__class__._current_runner + if _image_sentinel is not None: + if _install_image_shim(current_runner) == 0: + raise RuntimeError("LightX2V IMAGE input shim could not find a runner image reader") + if hasattr(current_runner, "set_progress_callback"): current_runner.set_progress_callback(update_progress) @@ -279,7 +285,7 @@ class LightX2VModularInferenceV2: if images.dtype != torch.float32: images = images.float() - if getattr(config, "unload_after_inference", False): + if config.get("unload_after_inference", False): current_runner = None # drop local ref so __del__ can run self._release_runner() else: @@ -292,5 +298,7 @@ class LightX2VModularInferenceV2: logging.error(f"Error during inference: {e}") raise finally: + if _image_sentinel is not None: + _release_image_sentinel(_image_sentinel) if _audio_sentinel is not None: _release_audio_sentinel(_audio_sentinel) diff --git a/nodes/seedvr.py b/nodes/seedvr.py index 256a3ac..25a3eeb 100644 --- a/nodes/seedvr.py +++ b/nodes/seedvr.py @@ -701,7 +701,7 @@ class LightX2VOutputVideoPreview: "AUDIO", {"tooltip": "Optional audio from Load Video/Get Video Components to merge into the output MP4."}, ), - } + }, } RETURN_TYPES = ("STRING",) diff --git a/nodes/swiftvr.py b/nodes/swiftvr.py index a394aa1..f7e6774 100644 --- a/nodes/swiftvr.py +++ b/nodes/swiftvr.py @@ -81,10 +81,7 @@ def _resolve_output_size(source_width: int, source_height: int, target_short_edg scale = target_short_edge / source_short_edge if scale > _MAX_SR_RATIO: - raise ValueError( - f"SwiftVR scale {scale:.3f}x exceeds the supported maximum {_MAX_SR_RATIO:.1f}x; " - f"lower target_short_edge" - ) + raise ValueError(f"SwiftVR scale {scale:.3f}x exceeds the supported maximum {_MAX_SR_RATIO:.1f}x; lower target_short_edge") if source_width <= source_height: output_width = target_short_edge output_height = int(round(source_height * scale)) @@ -97,8 +94,7 @@ def _resolve_output_size(source_width: int, source_height: int, target_short_edg output_height = max(2, (output_height + 1) // 2 * 2) if max(output_width, output_height) > _MAX_OUTPUT_DIMENSION: raise ValueError( - f"SwiftVR output {output_width}x{output_height} exceeds the maximum supported dimension " - f"{_MAX_OUTPUT_DIMENSION}; lower target_short_edge" + f"SwiftVR output {output_width}x{output_height} exceeds the maximum supported dimension {_MAX_OUTPUT_DIMENSION}; lower target_short_edge" ) return output_height, output_width, scale From 2df4f7b7ff306391a8bb2fc4c9d88b7369d34714 Mon Sep 17 00:00:00 2001 From: gaclove Date: Fri, 23 Oct 2026 15:27:11 +0800 Subject: [PATCH 6/6] chore: clean up project metadata and docs --- .github/workflows/publish.yml | 1 + .github/workflows/quality.yml | 27 ++++ CONTRIBUTING.md | 32 +++++ README.md | 229 +++++++++------------------------- README_CN.md | 223 ++++++++------------------------- file_handlers.py | 28 ----- pyproject.toml | 6 +- 7 files changed, 180 insertions(+), 366 deletions(-) create mode 100644 .github/workflows/quality.yml create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 68f039a..3edf6e0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,7 @@ on: - "v*.*.*" permissions: + contents: read issues: write jobs: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..ca8a76c --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,27 @@ +name: Quality checks + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + pre-commit: + name: Ruff and repository checks + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install pre-commit + run: python -m pip install pre-commit + + - name: Run checks + run: pre-commit run --all-files diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c501ca0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing + +## Development setup + +The wrapper follows the Python version supported by the bundled LightX2V +submodule (Python 3.10 or newer). + +```bash +git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git +cd ComfyUI-Lightx2vWrapper +python -m pip install -r lightx2v/requirements.txt +python -m pip install -r requirements.txt pre-commit +pre-commit install +``` + +## Before opening a pull request + +Run the repository checks from the project root: + +```bash +pre-commit run --all-files +``` + +For workflow or runner changes, validate the affected ComfyUI workflow with a +local LightX2V installation and include the workflow or reproduction details +in the pull request description. Keep changes to the `lightx2v` submodule in a +separate upstream contribution unless the wrapper specifically requires a +submodule revision update. + +Pull requests should explain the user-visible behavior, list validation +performed, and avoid committing model weights, generated media, credentials, +or local debugging artifacts. diff --git a/README.md b/README.md index 54e524d..869c336 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,97 @@ -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/) [![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom%20Node-brightgreen)](https://github.com/comfyanonymous/ComfyUI) [![Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20Models-lightx2v-yellow)](https://huggingface.co/lightx2v) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GacLove/ComfyUI-Lightx2vWrapper) +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) +[![ComfyUI](https://img.shields.io/badge/ComfyUI-Custom%20Node-brightgreen)](https://github.com/comfyanonymous/ComfyUI) +[![Models](https://img.shields.io/badge/models-LightX2V-yellow)](https://huggingface.co/lightx2v) # ComfyUI-Lightx2vWrapper -[中文版](./README_CN.md) | English +ComfyUI custom nodes for [LightX2V](https://github.com/ModelTC/LightX2V). The +wrapper exposes LightX2V's modular configuration and runner APIs, together +with ComfyUI-friendly file inputs and output previews. -A ComfyUI custom node wrapper for LightX2V, enabling modular video generation with advanced optimization features. +[中文文档](README_CN.md) -## Features +## Supported workflows -- **Modular Configuration System**: Separate nodes for each aspect of video generation -- **Text-to-Video (T2V) and Image-to-Video (I2V)**: Support for both generation modes -- **Advanced Optimizations**: - - TeaCache acceleration (up to 3x speedup) - - Quantization support (int8, fp8) - - Memory optimization with CPU offloading - - Lightweight VAE options -- **LoRA Support**: Chain multiple LoRA models for customization -- **Multiple Model Support**: wan2.1, hunyuan architectures +The repository includes runnable workflow examples under +[`example_workflows/`](example_workflows/): + +- [Wan 2.2](example_workflows/lighx2v_wan2.2.json) text/image-to-video +- [SekoTalk](example_workflows/lightx2v_seko_talk.json) and [static + SekoTalk](example_workflows/lightx2v_seko_talk_static.json) +- [Multi-person SekoTalk](example_workflows/lightx2v_seko_talk_multi_talk.json) +- [SeedVR2](example_workflows/lightx2v_seedvr.json) video restoration +- [SwiftVR image](example_workflows/lightx2v_swiftvr_image.json) and [SwiftVR + video](example_workflows/lightx2v_swiftvr_video.json) restoration + +The examples are versioned with the wrapper and are the best reference for +current node names and connections. ## Installation -1. Clone this repository with submodules into your ComfyUI's `custom_nodes` directory: +Clone the repository with its LightX2V submodule into ComfyUI's +`custom_nodes` directory: ```bash cd ComfyUI/custom_nodes -git clone --recursive https://github.com/gaclove/ComfyUI-Lightx2vWrapper.git +git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git +cd ComfyUI-Lightx2vWrapper +python -m pip install -r lightx2v/requirements.txt +python -m pip install -r requirements.txt ``` -If you already cloned without submodules, initialize them: +For an existing checkout, update the submodule with: ```bash -cd ComfyUI-Lightx2vWrapper git submodule update --init --recursive ``` -2. Install dependencies: +Download the model files from [LightX2V on Hugging Face](https://huggingface.co/lightx2v) +and place them under `ComfyUI/models/lightx2v/`. -```bash -cd ComfyUI-Lightx2vWrapper -# Install lightx2v submodule dependencies -pip install -r lightx2v/requirements.txt -# Install ComfyUI wrapper dependencies -pip install -r requirements.txt -``` +## Node groups -3. Download models and place them in `ComfyUI/models/lightx2v/` directory +- **Configuration:** `LightX2VInferenceConfig`, `LightX2VTeaCache`, + `LightX2VQuantization`, and `LightX2VMemoryOptimization`. +- **Composition:** `LightX2VConfigCombinerV2` and `LightX2VConfigCombinerV3`. +- **Inference:** `LightX2VModularInferenceV2`. +- **Talk inputs:** single-object, file-list, JSON, and multi-object talk + combiners. +- **Video inputs and restoration:** `LightX2VInputVideoPath`, SeedVR2 and + SwiftVR loaders/samplers, plus `LightX2VOutputVideoPreview`. +- **LoRA:** `LightX2VLoRALoader`. -## Node Overview +The inference node currently accepts the model classes exposed by +`model_utils.support_model_cls_list`: Wan 2.1/2.2 variants, SekoTalk, +Qwen Image, and SeedVR2. The retired `*_distill` values remain selectable as +compatibility aliases for saved ComfyUI workflows and are normalized before +LightX2V's runner is built. -### Configuration Nodes +## Model files -#### 1. LightX2V Inference Config +Place each model in its own directory under `ComfyUI/models/lightx2v/`. LoRA +files can be placed in either `ComfyUI/models/lightx2v/loras/` or +`ComfyUI/models/loras/`. -Basic inference configuration for video generation. - -- **Inputs**: model, task_type, inference_steps, seed, cfg_scale, width, height, video_length, fps -- **Output**: Base configuration object - -#### 2. LightX2V TeaCache - -Feature caching acceleration configuration. - -- **Inputs**: enable, threshold (0.0-1.0), use_ret_steps -- **Output**: TeaCache configuration -- **Note**: Lower threshold = more speedup (0.1 ~2x, 0.2 ~3x) - -#### 3. LightX2V Quantization - -Model quantization settings for memory efficiency. - -- **Inputs**: dit_precision, t5_precision, clip_precision, backend, sensitive_layers_precision -- **Output**: Quantization configuration -- **Backends**: Auto-detected (vllm, sgl, q8f) - -#### 4. LightX2V Memory Optimization - -Memory management strategies. - -- **Inputs**: optimization_level, attention_type, enable_rotary_chunking, cpu_offload, unload_after_generate -- **Output**: Memory optimization configuration - -#### 5. LightX2V Lightweight VAE - -VAE optimization options. - -- **Inputs**: use_tiny_vae, use_tiling_vae -- **Output**: VAE configuration - -#### 6. LightX2V LoRA Loader - -Load and chain LoRA models. - -- **Inputs**: lora_name, strength (0.0-2.0), lora_chain (optional) -- **Output**: LoRA chain configuration - -### Combination Node - -#### 7. LightX2V Config Combiner - -Combines all configuration modules into a single configuration. - -- **Inputs**: All configuration types (optional) -- **Output**: Combined configuration object - -### Inference Node - -#### 8. LightX2V Modular Inference - -Main inference node for video generation. - -- **Inputs**: combined_config, prompt, negative_prompt, image (optional), audio (optional) -- **Outputs**: Generated video frames - -## Usage Examples - -### Basic T2V Workflow - -1. Create LightX2V Inference Config (task_type: "t2v") -2. Use LightX2V Config Combiner -3. Connect to LightX2V Modular Inference with text prompt -4. Save video output - -### I2V with Optimizations - -1. Load input image -2. Create LightX2V Inference Config (task_type: "i2v") -3. Add LightX2V TeaCache (threshold: 0.26) -4. Add LightX2V Memory Optimization -5. Combine configs with LightX2V Config Combiner -6. Run LightX2V Modular Inference - -### With LoRA - -1. Create base configuration -2. Load LoRA with LightX2V LoRA Loader -3. Chain multiple LoRAs if needed -4. Combine all configs -5. Run inference - -## Model Directory Structure - -Download models from: - -Models should be placed in: - -```txt +```text ComfyUI/models/lightx2v/ -├── Wan2.1-I2V-14B-720P-xxx/ # Main model checkpoints -├── Wan2.1-I2V-14B-480P-xxx/ # Main model checkpoints -├── loras/ # LoRA models +├── Wan2.2-I2V-A14B-distill-fp8/ +├── seedvr2_ema_3b_fp8/ +└── loras/ ``` -## Tips +The exact directory names depend on the downloaded checkpoint. The model +picker only lists directories that exist on the local ComfyUI installation. -- Start with default settings and adjust based on your hardware -- Use TeaCache with threshold 0.1-0.2 for significant speedup -- Enable memory optimization if running on limited VRAM -- Quantization can reduce memory usage but may affect quality -- Chain multiple LoRAs for complex style combinations +## Development -## Troubleshooting - -- **Out of Memory**: Enable memory optimization or use quantization -- **Slow Generation**: Enable TeaCache or reduce inference steps -- **Model Not Found**: Check model paths in `ComfyUI/models/lightx2v/` - -## Example Workflows - -Example workflow JSON files are provided in the `examples/` directory: - -- `wan_i2v.json` - Basic image-to-video -- `wan_i2v_with_distill_lora.json` - I2V with distillation LoRA -- `wan_t2v_with_distill_lora.json` - T2V with distillation LoRA - -## Contributing Guidelines - -We welcome community contributions! Before submitting code, please ensure you follow these steps: - -### Install Development Dependencies - -```bash -pip install ruff pre-commit -``` - -### Code Quality Check - -Before committing code, run the following command: +Install the development hook and run the same checks used by CI: ```bash +python -m pip install pre-commit +pre-commit install pre-commit run --all-files ``` -This will automatically check code formatting, syntax errors, and other code quality issues. - -### Contribution Process - -1. Fork this repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Create a Pull Request +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for pull request and workflow +validation guidelines. diff --git a/README_CN.md b/README_CN.md index cec5be9..0ec281c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -1,202 +1,89 @@ -[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org/downloads/) [![ComfyUI](https://img.shields.io/badge/ComfyUI-自定义节点-brightgreen)](https://github.com/comfyanonymous/ComfyUI) [![Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20模型-lightx2v-yellow)](https://huggingface.co/lightx2v) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/GacLove/ComfyUI-Lightx2vWrapper) +[![许可证](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) +[![ComfyUI](https://img.shields.io/badge/ComfyUI-自定义节点-brightgreen)](https://github.com/comfyanonymous/ComfyUI) +[![模型](https://img.shields.io/badge/models-LightX2V-yellow)](https://huggingface.co/lightx2v) # ComfyUI-Lightx2vWrapper -[English](./README.md) | 中文版 +[English](README.md) | 中文 -LightX2V 的 ComfyUI 自定义节点封装,通过模块化配置实现视频生成和高级优化功能。 +[LightX2V](https://github.com/ModelTC/LightX2V) 的 ComfyUI 自定义节点封装, +提供模块化配置、ComfyUI 文件输入以及视频输出预览。 -## 功能特性 +## 已提供的工作流 -- **模块化配置系统**:为视频生成的各个方面提供独立的配置节点 -- **文本生成视频(T2V)和图像生成视频(I2V)**:支持两种生成模式 -- **高级优化功能**: - - TeaCache 加速(最高可达 3 倍速度提升) - - 量化支持(int8、fp8) - - 内存优化与 CPU 卸载 - - 轻量级 VAE 选项 -- **LoRA 支持**:可串联多个 LoRA 模型进行自定义 -- **多模型支持**:wan2.1、hunyuan 架构 +仓库在 [`example_workflows/`](example_workflows/) 中提供了与当前节点注册表 +同步的示例: -## 安装说明 +- [Wan 2.2](example_workflows/lighx2v_wan2.2.json) 文生视频/图生视频 +- [SekoTalk](example_workflows/lightx2v_seko_talk.json) 和[静态 + SekoTalk](example_workflows/lightx2v_seko_talk_static.json) +- [多人 SekoTalk](example_workflows/lightx2v_seko_talk_multi_talk.json) +- [SeedVR2](example_workflows/lightx2v_seedvr.json) 视频修复 +- [SwiftVR 图像](example_workflows/lightx2v_swiftvr_image.json) 和 [SwiftVR + 视频](example_workflows/lightx2v_swiftvr_video.json) 修复 -1. 将此仓库克隆到 ComfyUI 的 `custom_nodes` 目录,包含子模块: +示例工作流会随封装器一起更新,可作为当前节点名称和连线方式的参考。 + +## 安装 + +将仓库和 LightX2V 子模块一起克隆到 ComfyUI 的 `custom_nodes` 目录: ```bash cd ComfyUI/custom_nodes -git clone --recursive https://github.com/gaclove/ComfyUI-Lightx2vWrapper.git +git clone --recursive https://github.com/ModelTC/ComfyUI-Lightx2vWrapper.git +cd ComfyUI-Lightx2vWrapper +python -m pip install -r lightx2v/requirements.txt +python -m pip install -r requirements.txt ``` -如果已经克隆但未包含子模块,请初始化子模块: +如果已有仓库但还没有子模块: ```bash -cd ComfyUI-Lightx2vWrapper git submodule update --init --recursive ``` -2. 安装依赖: +从 [Hugging Face 的 LightX2V 页面](https://huggingface.co/lightx2v) 下载模型, +放到 `ComfyUI/models/lightx2v/` 下。 -```bash -cd ComfyUI-Lightx2vWrapper -# 安装 lightx2v 子模块依赖 -pip install -r lightx2v/requirements.txt -# 安装 ComfyUI 封装器依赖 -pip install -r requirements.txt -``` +## 节点分组 -3. 下载模型并放置在 `ComfyUI/models/lightx2v/` 目录 +- **配置:** `LightX2VInferenceConfig`、`LightX2VTeaCache`、 + `LightX2VQuantization`、`LightX2VMemoryOptimization` +- **组合:** `LightX2VConfigCombinerV2`、`LightX2VConfigCombinerV3` +- **推理:** `LightX2VModularInferenceV2` +- **Talk 输入:** 单对象、文件列表、JSON 和多人 Talk 组合节点 +- **视频输入与修复:** `LightX2VInputVideoPath`、SeedVR2/SwiftVR 加载器与 + 采样器、`LightX2VOutputVideoPreview` +- **LoRA:** `LightX2VLoRALoader` -## 节点概览 +推理节点当前支持 `model_utils.support_model_cls_list` 中的 Wan 2.1/2.2 +变体、SekoTalk、Qwen Image 和 SeedVR2。旧 workflow 使用的 `*_distill` +值仍作为兼容别名保留,构建 LightX2V runner 前会自动归一化。 -### 配置节点 +## 模型目录 -#### 1. LightX2V Inference Config(推理配置) +每个模型放在 `ComfyUI/models/lightx2v/` 下的独立目录中。LoRA 可以放在 +`ComfyUI/models/lightx2v/loras/` 或 `ComfyUI/models/loras/`: -视频生成的基础推理配置。 - -- **输入**:model(模型)、task_type(任务类型)、inference_steps(推理步数)、seed(种子)、cfg_scale(CFG 缩放)、width(宽度)、height(高度)、video_length(视频长度)、fps(帧率) -- **输出**:基础配置对象 - -#### 2. LightX2V TeaCache(TeaCache 配置) - -特征缓存加速配置。 - -- **输入**:enable(启用)、threshold(阈值 0.0-1.0)、use_ret_steps(使用返回步数) -- **输出**:TeaCache 配置 -- **注意**:阈值越低 = 速度提升越大(0.1 约 2 倍,0.2 约 3 倍) - -#### 3. LightX2V Quantization(量化配置) - -模型量化设置,提升内存效率。 - -- **输入**:dit_precision(DIT 精度)、t5_precision(T5 精度)、clip_precision(CLIP 精度)、backend(后端)、sensitive_layers_precision(敏感层精度) -- **输出**:量化配置 -- **后端**:自动检测(vllm、sgl、q8f) - -#### 4. LightX2V Memory Optimization(内存优化) - -内存管理策略。 - -- **输入**:optimization_level(优化级别)、attention_type(注意力类型)、enable_rotary_chunking(启用旋转分块)、cpu_offload(CPU 卸载)、unload_after_generate(生成后卸载) -- **输出**:内存优化配置 - -#### 5. LightX2V Lightweight VAE(轻量级 VAE) - -VAE 优化选项。 - -- **输入**:use_tiny_vae(使用微型 VAE)、use_tiling_vae(使用平铺 VAE) -- **输出**:VAE 配置 - -#### 6. LightX2V LoRA Loader(LoRA 加载器) - -加载并串联 LoRA 模型。 - -- **输入**:lora_name(LoRA 名称)、strength(强度 0.0-2.0)、lora_chain(LoRA 链,可选) -- **输出**:LoRA 链配置 - -### 组合节点 - -#### 7. LightX2V Config Combiner(配置组合器) - -将所有配置模块组合成单一配置。 - -- **输入**:所有配置类型(可选) -- **输出**:组合后的配置对象 - -### 推理节点 - -#### 8. LightX2V Modular Inference(模块化推理) - -视频生成的主推理节点。 - -- **输入**:combined_config(组合配置)、prompt(提示词)、negative_prompt(负面提示词)、image(图像,可选)、audio(音频,可选) -- **输出**:生成的视频帧 - -## 使用示例 - -### 基础 T2V 工作流 - -1. 创建 LightX2V Inference Config(task_type: "t2v") -2. 使用 LightX2V Config Combiner -3. 连接到 LightX2V Modular Inference,输入文本提示词 -4. 保存视频输出 - -### 带优化的 I2V - -1. 加载输入图像 -2. 创建 LightX2V Inference Config(task_type: "i2v") -3. 添加 LightX2V TeaCache(threshold: 0.26) -4. 添加 LightX2V Memory Optimization -5. 使用 LightX2V Config Combiner 组合配置 -6. 运行 LightX2V Modular Inference - -### 使用 LoRA - -1. 创建基础配置 -2. 使用 LightX2V LoRA Loader 加载 LoRA -3. 如需要可串联多个 LoRA -4. 组合所有配置 -5. 运行推理 - -## 模型目录结构 - -从以下地址下载模型: - -模型应放置在: - -```txt +```text ComfyUI/models/lightx2v/ -├── Wan2.1-I2V-14B-720P-xxx/ # 主模型检查点 -├── Wan2.1-I2V-14B-480P-xxx/ # 主模型检查点 -├── loras/ # LoRA 模型 +├── Wan2.2-I2V-A14B-distill-fp8/ +├── seedvr2_ema_3b_fp8/ +└── loras/ ``` -## 使用技巧 +具体目录名取决于下载的 checkpoint;模型选择器只会列出当前 ComfyUI 中存在 +的目录。 -- 从默认设置开始,根据硬件情况调整 -- 使用 TeaCache,阈值设为 0.1-0.2 可显著提速 -- 在显存有限时启用内存优化 -- 量化可减少内存使用但可能影响质量 -- 串联多个 LoRA 实现复杂风格组合 +## 开发 -## 故障排除 - -- **内存不足**:启用内存优化或使用量化 -- **生成速度慢**:启用 TeaCache 或减少推理步数 -- **找不到模型**:检查 `ComfyUI/models/lightx2v/` 中的模型路径 - -## 示例工作流 - -`examples/` 目录中提供了示例工作流 JSON 文件: - -- `wan_i2v.json` - 基础图像生成视频 -- `wan_i2v_with_distill_lora.json` - 带蒸馏 LoRA 的 I2V -- `wan_t2v_with_distill_lora.json` - 带蒸馏 LoRA 的 T2V - -## 贡献指南 - -我们欢迎社区贡献!在提交代码之前,请确保遵循以下步骤: - -### 安装开发依赖 - -```bash -pip install ruff pre-commit -``` - -### 代码质量检查 - -在提交代码之前,请运行以下命令: +安装 pre-commit 并运行 CI 使用的检查: ```bash +python -m pip install pre-commit +pre-commit install pre-commit run --all-files ``` -这将自动检查代码格式、语法错误和其他代码质量问题。 - -### 贡献流程 - -1. Fork 本仓库 -2. 创建功能分支 (`git checkout -b feature/amazing-feature`) -3. 提交更改 (`git commit -m 'Add some amazing feature'`) -4. 推送到分支 (`git push origin feature/amazing-feature`) -5. 创建 Pull Request +贡献流程和 workflow 验证要求见 [`CONTRIBUTING.md`](CONTRIBUTING.md)。 diff --git a/file_handlers.py b/file_handlers.py index 3e98d21..d003727 100644 --- a/file_handlers.py +++ b/file_handlers.py @@ -142,34 +142,6 @@ class AudioFileHandler(FileHandler): return waveform - def _save_with_wave(self, path: str, waveform: np.ndarray, sample_rate: int): - """Save audio using wave module.""" - import wave - - with wave.open(path, "wb") as wav_file: - wav_file.setnchannels(1 if waveform.ndim == 1 else waveform.shape[-1]) - wav_file.setsampwidth(2) # 16-bit - wav_file.setframerate(sample_rate) - - if waveform.dtype != np.int16: - waveform = (waveform * 32767).astype(np.int16) - - wav_file.writeframes(waveform.tobytes()) - - def _load_with_wave(self, path: str) -> Tuple[np.ndarray, int]: - """Load audio using wave module.""" - import wave - - with wave.open(path, "rb") as wav_file: - sample_rate = wav_file.getframerate() - frames = wav_file.readframes(wav_file.getnframes()) - waveform = np.frombuffer(frames, dtype=np.int16) - - if wav_file.getnchannels() > 1: - waveform = waveform.reshape(-1, wav_file.getnchannels()) - - return waveform, sample_rate - class ImageFileHandler(FileHandler): """Handler for image files.""" diff --git a/pyproject.toml b/pyproject.toml index 69a9c6c..617fc2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,8 @@ name = "lightx2vwrapper" description = "ComfyUI-Lightx2vWrapper is an inference wrapper for Lightx2v designed for use with ComfyUI." version = "1.1.0" +readme = "README.md" +requires-python = ">=3.10" license = { file = "LICENSE" } [project.urls] @@ -19,10 +21,8 @@ line-length = 150 indent-width = 4 [tool.ruff.lint] -extend-select = ["I", "F401"] -ignore = ["F"] +extend-select = ["I"] [tool.ruff.lint.per-file-ignores] "**/__init__.py" = ["F401"] "**/lightx2v_kernel/*" = ["F401"] -"**/{cookbook,docs}/*" = ["E402", "F401", "F811", "F841"]