| """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 |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
|
|
| 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" |
|
|
| |
|
|
| N_SAM3_REF = "2" |
| N_SAM3_DRIVE = "3" |
| N_DIFFUSION = "4" |
| N_CLIP = "5" |
| N_VAE = "6" |
| N_CLIP_VISION = "9" |
| N_SAM3_CKPT = "11" |
| N_TRANSITION = "12" |
| N_SAMPLER = "13" |
| N_COMBINE = "15" |
| N_SAM3_TEXT_REF = "16" |
| N_SAM3_TEXT_DRIVE = "17" |
| N_POSITIVE = "21" |
| N_NEGATIVE = "22" |
| N_SAMPLING_SD3 = "23" |
| N_POWER_LORA = "25" |
| N_COLORED_MASK = "29" |
| N_MULTI_REF = "50" |
| N_REF_SPLIT = "51" |
| N_RESOLUTION = "53" |
| N_LOAD_VIDEO = "67" |
| N_CONTEXT_WINDOWS = "75" |
| N_DIRECTOR = "80" |
|
|
| |
| N_LORA_1 = "251" |
| N_LORA_2 = "252" |
| REF_IMAGE_NODES = ["101", "102", "103", "104", "105", "106"] |
| MAX_REFS = len(REF_IMAGE_NODES) |
|
|
| |
| DROP_NODES = ["19", "20", "34", N_DIRECTOR, N_POWER_LORA] |
|
|
| OUTPUT_PREFIX = "scail2_mrs_v6" |
|
|
| |
| 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压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的," |
| "毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" |
| ) |
|
|
| |
|
|
| def _run(*cmd): |
| print("$", " ".join(str(c) for c in cmd), flush=True) |
| subprocess.run([str(c) for c in cmd], check=True) |
|
|
| |
|
|
| CUSTOM_REPOS = { |
| "ComfyUI-VideoHelperSuite": |
| "https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite", |
| "ComfyUI-KJNodes": |
| "https://github.com/kijai/ComfyUI-KJNodes", |
| |
| |
| "ComfyUI-WanAni-SQR": |
| "https://github.com/zere111ai/ComfyUI-WanAni-SQR", |
| } |
|
|
| |
| |
| 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"]) |
|
|
|
|
| |
| MODEL_SPECS = { |
| "diffusion": ( |
| "LHQAQ-Li/wan2.1_14B_SCAIL_2_nvfp4_comfy_V2", |
| "wan2.1_14B_SCAIL_2_nvfp4_comfy_V2.safetensors", |
| "diffusion_models", |
| ), |
| |
| "diffusion_fp8": ( |
| "Comfy-Org/SCAIL-2", |
| "diffusion_models/wan2.1_14B_SCAIL_2_fp8_scaled.safetensors", |
| "diffusion_models", |
| ), |
| |
| |
| "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", |
| ), |
| } |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
|
|
| |
|
|
| 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) |
|
|
| |
| |
| |
| 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 |
|
|
| |
| wf[N_POSITIVE]["inputs"]["text"] = "" |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| _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) |
|
|
| |
| |
| |
| 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 |
| 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)}") |
|
|
| |
| |
| |
| |
| 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 |
| } |
|
|
|
|
| |
|
|
| 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: |
| |
| 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 |
| + frames * 0.6 |
| + windows * int(steps) * 28 |
| + frames * 0.4 |
| ) |
| 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枚以上アップロードしてください。") |
|
|
| |
| 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), |
| ) |
|
|
| |
| _inject(wf, N_DIFFUSION, "model_name", diffusion_path.name) |
| |
| _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) |
|
|
| |
| _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)) |
|
|
| |
| _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") |
|
|
| |
| _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)) |
|
|
| |
| _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}") |
|
|
| |
| 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): |
| |
| if not args or not args[0]: |
| print("=== SCAIL2-MRS: generate() with no video (pre-warm) ===", flush=True) |
| return None |
| return _generate_gpu(*args) |
|
|
| |
|
|
| 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, |
| ) |
|
|
| |
| |
| 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() |
|
|