"""SCAIL-2 MultiRef Segmented v6 — HuggingFace ZeroGPU Space. Runs `scail2MultiRefSegmented_v6-2.json` (a ComfyUI API-format export) with ComfyUI driven in-process via `execution.PromptExecutor`. The exported workflow contains `WanAniDirector` (node 80), whose SegmentQueueRunner re-submits per-segment sub-jobs to a live ComfyUI HTTP server (`/prompt`, `/history`) and depends on `extra_pnginfo.sqr_full_prompt` injected by its frontend JS — neither exists here. It also dynamically creates the `LoadImage` nodes feeding `WanSQRMultiReference.image_1..6`, which is why those inputs are absent from the export. `_build_base_workflow()` performs the equivalent rewiring in Python; the export has 分段数=1, so a single segment is functionally equivalent. """ import os # Disable PyTorch's CUDA memory caching pool entirely. # ZeroGPU's `large` size is half an RTX Pro 6000 (a partitioned GPU); PyTorch's # caching allocator calls nvmlDeviceGetMemoryInfo (unsupported on partitions) in # mallocRetry when cudaMalloc fails, causing an assertion. cudaMallocAsync avoids # NVML but retains freed memory in its pool, accumulating tens of GB across model # loads/offloads within one GPU session. PYTORCH_NO_CUDA_MEMORY_CACHING bypasses # all pooling: every alloc is a direct cudaMalloc, every free a direct cudaFree — # NVML is never queried and offloaded models release VRAM immediately. os.environ.setdefault("PYTORCH_NO_CUDA_MEMORY_CACHING", "1") import copy import json import math import random import shutil import subprocess import sys import time from pathlib import Path # ─── Paths ─────────────────────────────────────────────────────────────────── APP_DIR = Path(__file__).parent WORKFLOW_JSON = APP_DIR / "scail2MultiRefSegmented_v6-2.json" COMFYUI_DIR = APP_DIR / "ComfyUI" CUSTOM_NODES_DIR = COMFYUI_DIR / "custom_nodes" INPUT_DIR = COMFYUI_DIR / "input" OUTPUT_DIR = COMFYUI_DIR / "output" # ─── Node IDs (from scail2MultiRefSegmented_v6-2.json) ─────────────────────── N_SAM3_REF = "2" # SAM3_VideoTrack (reference stills) N_SAM3_DRIVE = "3" # SAM3_VideoTrack (driving video) N_DIFFUSION = "4" # DiffusionModelLoaderKJ N_CLIP = "5" # CLIPLoader (umt5) N_VAE = "6" # VAELoader N_CLIP_VISION = "9" # CLIPVisionLoader N_SAM3_CKPT = "11" # CheckpointLoaderSimple (sam3.1) N_TRANSITION = "12" # SQRSCAIL2TransitionToVideo N_SAMPLER = "13" # KSampler N_COMBINE = "15" # VHS_VideoCombine (the output node) N_SAM3_TEXT_REF = "16" # CLIPTextEncode (reference subject prompt) N_SAM3_TEXT_DRIVE = "17" # CLIPTextEncode (driving subject prompt) N_POSITIVE = "21" # CLIPTextEncode (positive; was fed by node 80) N_NEGATIVE = "22" # CLIPTextEncode (negative) N_SAMPLING_SD3 = "23" # ModelSamplingSD3 N_POWER_LORA = "25" # Power Lora Loader (rgthree) — replaced N_COLORED_MASK = "29" # SQRScail2ColoredMaskAdvanced N_MULTI_REF = "50" # WanSQRMultiReference N_REF_SPLIT = "51" # SQRScail2ReferenceBatchSplit N_RESOLUTION = "53" # LHResolutionSetting N_LOAD_VIDEO = "67" # VHS_LoadVideo N_CONTEXT_WINDOWS = "75" # WanContextWindowsManual N_DIRECTOR = "80" # WanAniDirector — removed # Nodes we synthesise to replace node 25 / the Director's LoadImage injection. N_LORA_1 = "251" N_LORA_2 = "252" REF_IMAGE_NODES = ["101", "102", "103", "104", "105", "106"] MAX_REFS = len(REF_IMAGE_NODES) # PreviewImage nodes — pure UI, wasted compute headless. DROP_NODES = ["19", "20", "34", N_DIRECTOR, N_POWER_LORA] OUTPUT_PREFIX = "scail2_mrs_v6" # LHResolutionSetting.RESOLUTIONS keys (ComfyUI-WanAni-SQR/scail_reference_nodes.py) RESOLUTION_CHOICES = [ "1:1 480p - 480 x 480", "1:1 720p - 720 x 720", "1:1 1024 - 1024 x 1024", "4:3 480p - 640 x 480", "4:3 768p - 1024 x 768", "16:9 480p - 854 x 480", "16:9 480p safe - 848 x 480", "16:9 720p - 1280 x 720", "21:9 480p - 1120 x 480", "21:9 720p - 1680 x 720", ] ORIENTATION_CHOICES = ["竖屏 Portrait", "横屏 Landscape"] IDENTITY_MODES = [ "multi_person", "single_person_multi_reference", "multi_person_multi_reference", ] SORT_BY_CHOICES = ["area", "left_to_right", "none"] PRECISION_CHOICES = ["nvfp4 (RTX Pro 6000 / Blackwell)", "fp8_scaled"] DEFAULT_NEGATIVE = ( "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量," "低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的," "毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" ) # ─── Helper ─────────────────────────────────────────────────────────────────── def _run(*cmd): print("$", " ".join(str(c) for c in cmd), flush=True) subprocess.run([str(c) for c in cmd], check=True) # ─── Phase 1: repos + model download (no CUDA) ─────────────────────────────── CUSTOM_REPOS = { "ComfyUI-VideoHelperSuite": "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite", "ComfyUI-KJNodes": "https://github.com/kijai/ComfyUI-KJNodes", # SQRSCAIL2TransitionToVideo, SQRScail2ColoredMaskAdvanced, # SQRScail2ReferenceBatchSplit, WanSQRMultiReference, LHResolutionSetting "ComfyUI-WanAni-SQR": "https://github.com/zere111ai/ComfyUI-WanAni-SQR", } # ComfyUI-WanAni-SQR pins `opencv-python>=4.8`; the GUI build is useless in a Space # and conflicts with opencv-python-headless from requirements.txt. SKIP_REQUIREMENTS = {"ComfyUI-WanAni-SQR"} def _setup_repos(): """Clone ComfyUI and the custom nodes the workflow needs.""" if not COMFYUI_DIR.exists(): print("Cloning ComfyUI (Comfy-Org master)…") _run("git", "clone", "--depth=1", "https://github.com/Comfy-Org/ComfyUI", COMFYUI_DIR) _run("pip", "install", "-r", COMFYUI_DIR / "requirements.txt", "-q") CUSTOM_NODES_DIR.mkdir(parents=True, exist_ok=True) INPUT_DIR.mkdir(parents=True, exist_ok=True) OUTPUT_DIR.mkdir(parents=True, exist_ok=True) for name, url in CUSTOM_REPOS.items(): dest = CUSTOM_NODES_DIR / name if dest.exists(): continue print(f"Cloning {name}…") _run("git", "clone", "--depth=1", url, dest) req = dest / "requirements.txt" if req.exists() and name not in SKIP_REQUIREMENTS: subprocess.run(["pip", "install", "-r", str(req), "-q"]) # repo_id, filename, folder_paths type MODEL_SPECS = { "diffusion": ( "LHQAQ-Li/wan2.1_14B_SCAIL_2_nvfp4_comfy_V2", "wan2.1_14B_SCAIL_2_nvfp4_comfy_V2.safetensors", "diffusion_models", ), # Downloaded on demand — see _model_path("diffusion_fp8"). "diffusion_fp8": ( "Comfy-Org/SCAIL-2", "diffusion_models/wan2.1_14B_SCAIL_2_fp8_scaled.safetensors", "diffusion_models", ), # The export names `nsfw_wan_umt5-xxl_fp8_scaled.safetensors`, which is not on # the Hub; the stock scaled fp8 umt5-xxl is the equivalent text encoder. "text_encoder": ( "Comfy-Org/Wan_2.1_ComfyUI_repackaged", "split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors", "text_encoders", ), "vae": ( "Comfy-Org/Wan_2.1_ComfyUI_repackaged", "split_files/vae/wan_2.1_vae.safetensors", "vae", ), "clip_vision": ( "Comfy-Org/Wan_2.1_ComfyUI_repackaged", "split_files/clip_vision/clip_vision_h.safetensors", "clip_vision", ), "sam3": ( "Comfy-Org/sam3.1", "checkpoints/sam3.1_multiplex_fp16.safetensors", "checkpoints", ), "lora_lightx2v": ( "Kijai/WanVideo_comfy", "Lightx2v/lightx2v_I2V_14B_480p_cfg_step_distill_rank64_bf16.safetensors", "loras", ), "lora_dpo": ( "Comfy-Org/SCAIL-2", "loras/wan2.1_SCAIL_2_DPO_lora_bf16.safetensors", "loras", ), } # fp8_scaled is a 14 GB alternative to the nvfp4 default; only fetch it if asked for. LAZY_MODELS = {"diffusion_fp8"} _MODEL_PATHS: dict[str, Path] = {} def _model_path(key: str) -> Path: """Resolve (downloading and registering on first use) one model file.""" if key in _MODEL_PATHS: return _MODEL_PATHS[key] from huggingface_hub import hf_hub_download repo_id, filename, model_type = MODEL_SPECS[key] print(f"Ensuring {Path(filename).name}…", flush=True) path = Path(hf_hub_download(repo_id=repo_id, filename=filename)) _MODEL_PATHS[key] = path # A lazily fetched model lands in a snapshot dir ComfyUI has not seen yet. if _comfyui_ready: import folder_paths folder_paths.add_model_folder_path(model_type, str(path.parent)) return path def _download_models(): for key in MODEL_SPECS: if key not in LAZY_MODELS: _model_path(key) def _diffusion_key(model_precision: str) -> str: return "diffusion_fp8" if model_precision == "fp8_scaled" else "diffusion" def _prefetch_precision(model_precision: str) -> str: """Fetch a lazily-downloaded diffusion model outside the GPU-billed window. Wired to the precision dropdown's change event: a 14 GB download inside @spaces.GPU would burn ZeroGPU quota and likely blow the duration budget. """ key = _diffusion_key(model_precision) if key in _MODEL_PATHS: return model_precision print(f"Prefetching {key} outside GPU context…", flush=True) _model_path(key) return model_precision # ─── Phase 2: graph rewrite (no CUDA, no ComfyUI import) ───────────────────── def _build_base_workflow(lora_1_on: bool, lora_1_strength: float, lora_2_on: bool, lora_2_strength: float, ref_count: int) -> dict: """Turn the Director-driven export into a self-contained API prompt. Drops the PreviewImage nodes, WanAniDirector and the rgthree Power Lora Loader; replaces the latter with core LoraLoaderModelOnly nodes and wires `ref_count` LoadImage nodes into WanSQRMultiReference.image_1..N. """ with open(WORKFLOW_JSON, encoding="utf-8") as f: wf: dict = json.load(f) for node_id in DROP_NODES: wf.pop(node_id, None) # node 25 (rgthree, MODEL+CLIP) fed only node 23's MODEL input; its CLIP output # was unused (nodes 21/22 take CLIP straight from node 5). Two core # LoraLoaderModelOnly nodes reproduce the two enabled LoRAs from lora_1/lora_4. model_ref = [N_DIFFUSION, 0] for node_id, enabled, key, strength in ( (N_LORA_1, lora_1_on, "lora_lightx2v", lora_1_strength), (N_LORA_2, lora_2_on, "lora_dpo", lora_2_strength), ): if not enabled: continue wf[node_id] = { "inputs": { "lora_name": _model_path(key).name, "strength_model": float(strength), "model": model_ref, }, "class_type": "LoraLoaderModelOnly", "_meta": {"title": f"LoRA {key}"}, } model_ref = [node_id, 0] wf[N_SAMPLING_SD3]["inputs"]["model"] = model_ref # The Director produced node 21's text; it is now a plain widget value. wf[N_POSITIVE]["inputs"]["text"] = "" # The Director also created these LoadImage nodes and linked them. for slot in range(1, MAX_REFS + 1): wf[N_MULTI_REF]["inputs"].pop(f"image_{slot}", None) for slot in range(1, ref_count + 1): node_id = REF_IMAGE_NODES[slot - 1] wf[node_id] = { "inputs": {"image": ""}, "class_type": "LoadImage", "_meta": {"title": f"Reference Image {slot}"}, } wf[N_MULTI_REF]["inputs"][f"image_{slot}"] = [node_id, 0] return wf # ─── Phase 3: ComfyUI init (deferred until first @spaces.GPU call) ─────────── # model_management.py calls torch.cuda.current_device() at import time → crashes # without a real GPU. Defer all ComfyUI imports to the GPU context. _comfyui_ready = False def _init_comfyui(): """Import ComfyUI, register model paths, load nodes. Runs once per worker.""" global _comfyui_ready if _comfyui_ready: return if str(COMFYUI_DIR) not in sys.path: sys.path.insert(0, str(COMFYUI_DIR)) import folder_paths folder_paths.base_path = str(COMFYUI_DIR) # Set PromptServer.instance BEFORE loading nodes: VHS, KJNodes and # ComfyUI-WanAni-SQR all register aiohttp routes on this singleton at import # time (segment_queue_node.py does `@server.PromptServer.instance.routes.get`). import server as comfy_server class _RouteTableDef: """Mimics aiohttp.web.RouteTableDef — supports @routes.get('/path').""" def __getattr__(self, method): def route(path, **kwargs): def decorator(handler): return handler return decorator return route class _MockRouter: frozen = True # stops KJNodes freezing an already-frozen router def __getattr__(self, name): return lambda *args, **kwargs: None class _MockApp: router = _MockRouter() def __getattr__(self, name): return lambda *args, **kwargs: None class _MockQueue: def __getattr__(self, name): return lambda *args, **kwargs: None class _MockPromptServer: client_id = None routes = _RouteTableDef() app = _MockApp() prompt_queue = _MockQueue() def send_sync(self, *args, **kwargs): pass def queue_updated(self): pass def __getattr__(self, name): return lambda *args, **kwargs: None comfy_server.PromptServer.instance = _MockPromptServer() for key, path in _MODEL_PATHS.items(): folder_paths.add_model_folder_path(MODEL_SPECS[key][2], str(path.parent)) import asyncio import nodes as comfy_nodes asyncio.run(comfy_nodes.init_extra_nodes(init_custom_nodes=True)) missing = _missing_node_classes(comfy_nodes.NODE_CLASS_MAPPINGS) if missing: raise RuntimeError(f"Node classes failed to load: {sorted(missing)}") # Force LOW_VRAM mode: offload models aggressively between nodes. ZeroGPU # reports the full RTX Pro 6000 to ComfyUI, so it picks HIGH_VRAM and tries to # keep the diffusion model, umt5, CLIP Vision, SAM3 and the VAE resident # simultaneously — over the per-request limit for the `large` partition. import comfy.model_management as _mm _mm.vram_state = _mm.VRAMState.LOW_VRAM print(f"=== SCAIL2-MRS: VRAM mode set to {_mm.vram_state} ===", flush=True) _comfyui_ready = True print("=== SCAIL2-MRS: ComfyUI ready ===", flush=True) def _missing_node_classes(node_class_mappings: dict) -> set[str]: """class_types the rewritten workflow needs but ComfyUI did not register.""" wf = _build_base_workflow(True, 1.0, True, 1.0, MAX_REFS) return { node["class_type"] for node in wf.values() if node["class_type"] not in node_class_mappings } # ─── Inference ──────────────────────────────────────────────────────────────── def _inject(workflow: dict, node_id: str, key: str, value): if node_id in workflow: workflow[node_id]["inputs"][key] = value else: print(f"⚠ Node {node_id} not found, skipping {key!r}", flush=True) def _stage_input(path: str | os.PathLike) -> str: """Copy a Gradio upload into ComfyUI/input/ and return its bare filename.""" INPUT_DIR.mkdir(parents=True, exist_ok=True) name = Path(path).name dest = INPUT_DIR / name if Path(path).resolve() != dest.resolve(): shutil.copy(path, dest) return name def _ref_paths(ref_files) -> list[str]: """Normalise the gr.Files / gr.Gallery value into at most MAX_REFS paths.""" if not ref_files: return [] paths = [] for item in ref_files: # gr.Gallery yields (path, caption) tuples; gr.Files yields str or FileData. if isinstance(item, (tuple, list)): item = item[0] paths.append(str(getattr(item, "name", item))) return paths[:MAX_REFS] class _NullServer: """Minimal PromptServer mock for library-mode execution.""" client_id = None def __getattr__(self, name): return lambda *args, **kwargs: None def _estimate_duration(frame_load_cap, steps, context_length, context_overlap): """Seconds of GPU time to request, clamped to ZeroGPU's practical ceiling.""" frames = max(1, int(frame_load_cap)) stride = max(1, int(context_length) - int(context_overlap)) windows = max(1, math.ceil(max(0, frames - int(context_overlap)) / stride)) est = ( 150 # model loads / offloads + frames * 0.6 # SAM3 tracking, reference + driving passes + windows * int(steps) * 28 # sampling + frames * 0.4 # VAE decode + mp4 mux ) return int(min(600, max(120, est))) def _generate_inner( video_path, ref_files, positive_prompt, negative_prompt, sam3_ref_prompt, sam3_drive_prompt, orientation, resolution, force_rate, frame_load_cap, seed, steps, cfg, identity_mode, sort_by, main_index, background_indices, context_length, context_overlap, model_precision, lora_1_on, lora_1_strength, lora_2_on, lora_2_strength, ): _t0 = time.time() def _log(msg): print(f"=== SCAIL2-MRS [{time.time() - _t0:6.1f}s]: {msg} ===", flush=True) if not video_path: raise ValueError("入力動画をアップロードしてください。") ref_paths = _ref_paths(ref_files) if not ref_paths: raise ValueError("参照画像を1枚以上アップロードしてください。") # Already resolved outside the GPU window by _prefetch_precision(). diffusion_path = _model_path(_diffusion_key(model_precision)) _init_comfyui() _log("ComfyUI init done") import execution as comfy_execution wf = _build_base_workflow( bool(lora_1_on), float(lora_1_strength), bool(lora_2_on), float(lora_2_strength), len(ref_paths), ) # Model files _inject(wf, N_DIFFUSION, "model_name", diffusion_path.name) # sage_attention defaults to "auto" in the export; sageattention is not installed. _inject(wf, N_DIFFUSION, "sage_attention", "disabled") _inject(wf, N_CLIP, "clip_name", _model_path("text_encoder").name) _inject(wf, N_VAE, "vae_name", _model_path("vae").name) _inject(wf, N_CLIP_VISION, "clip_name", _model_path("clip_vision").name) _inject(wf, N_SAM3_CKPT, "ckpt_name", _model_path("sam3").name) # Inputs _inject(wf, N_LOAD_VIDEO, "video", _stage_input(video_path)) _inject(wf, N_LOAD_VIDEO, "force_rate", int(force_rate)) _inject(wf, N_LOAD_VIDEO, "frame_load_cap", int(frame_load_cap)) for slot, ref_path in enumerate(ref_paths, start=1): _inject(wf, REF_IMAGE_NODES[slot - 1], "image", _stage_input(ref_path)) # Prompts _inject(wf, N_POSITIVE, "text", positive_prompt or "") _inject(wf, N_NEGATIVE, "text", negative_prompt or "") _inject(wf, N_SAM3_TEXT_REF, "text", sam3_ref_prompt or "human") _inject(wf, N_SAM3_TEXT_DRIVE, "text", sam3_drive_prompt or "human") # Resolution / sampling _inject(wf, N_RESOLUTION, "orientation", orientation) _inject(wf, N_RESOLUTION, "resolution", resolution) if int(seed) < 0: seed = random.randint(0, 2**53 - 1) _inject(wf, N_SAMPLER, "seed", int(seed)) _inject(wf, N_SAMPLER, "steps", int(steps)) _inject(wf, N_SAMPLER, "cfg", float(cfg)) # Multi-reference identity handling _inject(wf, N_COLORED_MASK, "identity_mode", identity_mode) _inject(wf, N_COLORED_MASK, "sort_by", sort_by) _inject(wf, N_COLORED_MASK, "background_indices", background_indices or "") _inject(wf, N_REF_SPLIT, "main_index", max(0, min(int(main_index), len(ref_paths) - 1))) _inject(wf, N_CONTEXT_WINDOWS, "context_length", int(context_length)) _inject(wf, N_CONTEXT_WINDOWS, "context_overlap", int(context_overlap)) _inject(wf, N_COMBINE, "filename_prefix", OUTPUT_PREFIX) import nest_asyncio nest_asyncio.apply() import torch if torch.cuda.is_available(): props = torch.cuda.get_device_properties(0) _log(f"GPU: {props.name}, total={props.total_memory / 1024**3:.1f}GB, " f"capability=sm_{props.major}{props.minor}") executor = comfy_execution.PromptExecutor( server=_NullServer(), cache_args={"ram": 0, "ram_inactive": 0}, ) _log(f"executing: {len(ref_paths)} refs, {frame_load_cap} frames, seed={seed}") before = set(OUTPUT_DIR.glob(f"{OUTPUT_PREFIX}*")) result = executor.execute( wf, prompt_id="gradio_run", extra_data={}, execute_outputs=[N_COMBINE], ) _log(f"execute() done, result={result!r}") # Newer ComfyUI returns (success, error, node_errors). if isinstance(result, tuple) and len(result) >= 2 and result[1]: raise RuntimeError(f"ComfyUI execution error: {result[1]}") new_videos = sorted( (p for p in OUTPUT_DIR.glob(f"{OUTPUT_PREFIX}*") if p not in before and p.suffix in (".mp4", ".webm")), key=lambda p: p.stat().st_mtime, ) if not new_videos: raise RuntimeError("ComfyUI produced no video file.") _log(f"output: {new_videos[-1]}") return str(new_videos[-1]) print("=== SCAIL2-MRS: cloning repos… ===", flush=True) _setup_repos() print("=== SCAIL2-MRS: downloading models… ===", flush=True) _download_models() print("=== SCAIL2-MRS: models ready ===", flush=True) GENERATE_PARAMS = ( "video_path", "ref_files", "positive_prompt", "negative_prompt", "sam3_ref_prompt", "sam3_drive_prompt", "orientation", "resolution", "force_rate", "frame_load_cap", "seed", "steps", "cfg", "identity_mode", "sort_by", "main_index", "background_indices", "context_length", "context_overlap", "model_precision", "lora_1_on", "lora_1_strength", "lora_2_on", "lora_2_strength", ) def _get_duration(*args): kw = dict(zip(GENERATE_PARAMS, args)) return _estimate_duration(kw["frame_load_cap"], kw["steps"], kw["context_length"], kw["context_overlap"]) try: import spaces as _spaces @_spaces.GPU(duration=_get_duration) def _generate_gpu(*args): import traceback try: return _generate_inner(*args) except Exception: traceback.print_exc() raise except ImportError: _generate_gpu = _generate_inner def generate(*args): # ZeroGPU pre-warms with empty inputs; don't treat that as a failure. if not args or not args[0]: print("=== SCAIL2-MRS: generate() with no video (pre-warm) ===", flush=True) return None return _generate_gpu(*args) # ─── Gradio UI ──────────────────────────────────────────────────────────────── import gradio as gr with gr.Blocks(title="SCAIL-2 MultiRef Segmented v6") as demo: gr.Markdown( "# SCAIL-2 MultiRef Segmented v6\n" "WAN2.1 14B SCAIL-2 (NVFP4) — 駆動動画のモーションを最大6枚の参照画像に転写します。\n" "SAM3 が参照画像側と駆動動画側の被写体をトラッキングし、色分けマスクとして " "SCAIL-2 に渡します。" ) with gr.Row(): with gr.Column(scale=1): inp_video = gr.Video(label="駆動動画 (Driving Video)") inp_refs = gr.Files( label=f"参照画像 (Reference Images, 最大 {MAX_REFS} 枚)", file_types=["image"], ) gal_refs = gr.Gallery(label="参照画像プレビュー", columns=3, height=180, show_label=False) inp_positive = gr.Textbox( label="Positive Prompt", placeholder="女人在跳舞 / a woman is dancing", lines=3, ) inp_negative = gr.Textbox( label="Negative Prompt", value=DEFAULT_NEGATIVE, lines=3, ) btn = gr.Button("Generate", variant="primary") with gr.Column(scale=1): out_video = gr.Video(label="生成結果") with gr.Accordion("解像度・フレーム", open=True): inp_orientation = gr.Radio( ORIENTATION_CHOICES, value="竖屏 Portrait", label="向き", ) inp_resolution = gr.Dropdown( RESOLUTION_CHOICES, value="16:9 480p safe - 848 x 480", label="解像度 (縦向きでは長辺・短辺が入れ替わります)", ) inp_force_rate = gr.Slider( 8, 30, value=24, step=1, label="force_rate (入力動画の再サンプル fps)", ) inp_frame_load_cap = gr.Slider( 17, 240, value=144, step=4, label="frame_load_cap (生成フレーム数 — 初回は 144 推奨)", ) with gr.Accordion("多参照・SAM3", open=False): inp_identity_mode = gr.Dropdown( IDENTITY_MODES, value="multi_person", label="identity_mode", info="single_person_multi_reference: 1人を複数参照で表現", ) inp_sort_by = gr.Dropdown( SORT_BY_CHOICES, value="area", label="sort_by (被写体の並び順)", ) inp_main_index = gr.Slider( 0, MAX_REFS - 1, value=0, step=1, label="main_index (主参照にする画像の 0 始まりの番号)", ) inp_background_indices = gr.Textbox( label="background_indices", placeholder="背景として扱う参照画像の 1 始まり番号 (例: 2 または 1,4)", ) inp_sam3_ref = gr.Textbox( label="SAM3 参照側プロンプト", value="一个女人", ) inp_sam3_drive = gr.Textbox( label="SAM3 駆動側プロンプト", value="human", ) with gr.Accordion("サンプラー・LoRA・モデル", open=False): inp_seed = gr.Number(value=-1, precision=0, label="seed (-1 でランダム)") inp_steps = gr.Slider(1, 12, value=4, step=1, label="steps") inp_cfg = gr.Slider(1.0, 8.0, value=1.0, step=0.1, label="cfg") inp_context_length = gr.Slider( 33, 81, value=81, step=4, label="context_length", ) inp_context_overlap = gr.Slider( 0, 32, value=16, step=4, label="context_overlap", ) inp_precision = gr.Dropdown( PRECISION_CHOICES, value=PRECISION_CHOICES[0], label="拡散モデル精度", info="fp8_scaled は初回選択時に約14GBを追加ダウンロードします", ) inp_lora_1_on = gr.Checkbox( value=True, label="LoRA: lightx2v I2V 480p cfg-step-distill", ) inp_lora_1_strength = gr.Slider( 0.0, 1.5, value=1.0, step=0.05, label="lightx2v strength", ) inp_lora_2_on = gr.Checkbox( value=True, label="LoRA: SCAIL-2 DPO", ) inp_lora_2_strength = gr.Slider( 0.0, 1.5, value=1.0, step=0.05, label="SCAIL-2 DPO strength", ) inp_refs.change( fn=lambda files: _ref_paths(files), inputs=inp_refs, outputs=gal_refs, ) # Download a non-default diffusion model as soon as it is picked, so it never # happens inside the GPU-billed window. inp_precision.change( fn=_prefetch_precision, inputs=inp_precision, outputs=inp_precision, ) btn.click( fn=generate, inputs=[ inp_video, inp_refs, inp_positive, inp_negative, inp_sam3_ref, inp_sam3_drive, inp_orientation, inp_resolution, inp_force_rate, inp_frame_load_cap, inp_seed, inp_steps, inp_cfg, inp_identity_mode, inp_sort_by, inp_main_index, inp_background_indices, inp_context_length, inp_context_overlap, inp_precision, inp_lora_1_on, inp_lora_1_strength, inp_lora_2_on, inp_lora_2_strength, ], outputs=out_video, ) if __name__ == "__main__": demo.queue().launch()