「LongLive-Plug-Wan2.2-TI2V-5B-cfg」動画生成モデル:当サイトの生成動画・配布ファイル一覧

2026年10月2日

「LongLive-Plug-Wan2.2-TI2V-5B-cfg」動画生成モデル:当サイトの生成動画・配布ファイル一覧

基本情報

項目 内容
リポジトリ Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-cfg
公開元のまとめ Efficient Large Model(NVIDIA・MIT) のモデルとライセンスのまとめ
公開日 2026-09-29
ライセンス apache-2.0
配布形式 safetensors
出典の種類 公開元の一次情報(HuggingFace公式リポジトリから直接取得)

情報を集めた時点で、当サイトのコードが確定させた値です。日付はJSTで表示しています。

当サイトで確かめたこと

  • クラウドの GPU を借りて、当サイトの固定のプロンプトから生成した動画を、選別せずに載せています。

詳しい値と条件は、下の「当サイトでの実測」の節にあります。

概要

テキストおよび画像から動画を生成するモデル「Wan2.2-TI2V-5B」に向けて、CFG(Classifier-Free Guidance)蒸留を施したLoRAアダプター「LongLive-Plug-Wan2.2-TI2V-5B-cfg」が公開されました。

本モデルは、ベースモデルの標準的なCFG=5による誘導フローを、生徒モデル側のCFG=1における1回の条件付きフォワードパスへと蒸留したアダプターです。デノイジングのステップごとに無条件(ネガティブ)分岐を計算する必要がなくなり、各ステップでDiT(Diffusion Transformer)のフォワード処理を1回のみに削減できます。なお、デノイジングスケジュールの蒸留は行われておらず、数ステップ生成を行うDMD(Distribution Matching Distillation)チェックポイントとは異なります。

スペック

モデルカードに記載されている仕様および推奨設定は以下の通りです。

  • パラメータ数: LoRAパラメータ数は161,218,560(FP32テンソル数: 600、ベースモデルは50億パラメータ規模の「Wan2.2-TI2V-5B」)
  • アーキテクチャ: トランスフォーマーの全30ブロックにまたがる300箇所のLinear層(各ブロックのself-attention q/k/v/o、cross-attention q/k/v/o、FFN 0/2)を対象としたPEFT LoRA(rank: 64、alpha: 64、dropout: 0.0)
  • 出力の仕様: 解像度1280×704(ベースモデル仕様として704×1280にも対応)、評価プロファイルでは121フレーム・24 FPS(元モデル仕様では5秒尺)
  • 推奨ステップ数・サンプラー: 50ステップ、FlowUniPCスケジューラ(timestep shift: 5.0)
  • 推奨ガイダンス・ウェイト設定: guidance_scaleは1.0(CFG=1)、LoRAウェイトスケールは1.0(学習時の整合スケール)
  • ライセンス条件: Apache-2.0ライセンスが適用されており、商用利用を含めて規定の許諾条件に従った利用が可能です

性能・品質

本アダプターの蒸留設計および推論設定に関して、公開元から以下の設定値が公開されています。

Setting Value
Teacher CFG scale used for distillation 5.0
Student CFG during training 1.0
Recommended inference CFG 1.0
Default LoRA weight scale 1.0
Teacher → student denoising steps 50 → 50
Scheduler / timestep shift FlowUniPC / 5.0
LoRA rank / alpha 64 / 64
Training checkpoint step 250

本手法では、固定された教師モデルの軌道状態に対して相対ガイダンス重み付けMSEを最小化する学習が行われており、ステップ数は元モデルと同じネイティブな50ステップを維持しています。学習時に生徒モデル自身によるロールアウト生成を行わない設計となっており、DMD criticなども含まれていません。無条件分岐の評価を省くことで計算負荷を抑えられる利点がある一方で、公開元はアーティファクトの完全性と実行された学習仕様を記録した研究用チェックポイントであるとしており、あらゆるプロンプトや下流モデルでの汎用的な品質を保証するものではないと言及しています。

なお、ベースとなっている「Wan2.2-TI2V-5B」自体の性能として、16×16×4(時間・縦・横方向で4×16×16)の圧縮率を持つWan2.2-VAEを採用しており、パッチ化層と合わせて4×32×32の総合圧縮を実現しています。元モデルの公表情報では、最適化を行わない状態でもコンシューマー向けGPU単体で5秒間の720P動画を9分未満で生成できる効率性を備えているとされています。

得意なこと・想定用途

本アダプターは、ベースモデルである「Wan2.2-TI2V-5B」の機能を維持しつつ、推論時の計算効率を向上させることを目的としています。具体的には、テキストからの動画生成(text-to-video)および画像からの動画生成(image-to-video)の両方のタスクにおいて、CFG(Classifier-Free Guidance)による誘導プロセスを効率化します。

従来のCFGを用いた推論では、各デノイジングステップにおいて「条件付き」と「無条件(ネガティブ)」の2回のフォワードパスが必要でしたが、本LoRAを適用して guidance_scale: 1.0 で実行することで、各ステップにつき1回の条件付きフォワードパスのみで動作させることが可能になります。これにより、生成品質の傾向を維持したまま、推論の計算コストを削減できることが期待されます。

配布ファイル

Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-cfg で配布されている重みファイルを、Hugging FaceのAPIから当サイトが並べたものです。サイズは実ファイルの値です。

ファイル サイズ
adapter_model.safetensors 645MB
generator_lora.pt 645MB

入手方法

本モデルは、Hugging Faceのリポジトリから入手可能です。配布形式として、以下のファイルが提供されています。

  • adapter_model.safetensors: 安全でポータブルな形式のgenerator LoRA
  • generator_lora.pt: LongLiveのネイティブペイロード
  • adapter_config.json: PEFT設定および対象モジュール情報
  • training_config.yaml: 学習設定の詳細
  • inference_overrides.yaml: 推論用の設定(50ステップ、CFG=1)
  • release_metadata.json: メタデータ
  • provenance.json: 出所情報およびチェックサム
  • SHA256SUMS: アーティファクトのチェックサム

Hugging Faceの huggingface_hub ライブラリを使用して、以下のコマンドで generator_lora.pt をダウンロードできます。

from huggingface_hub import hf_hub_download

lora_path = hf_hub_download(
    repo_id="Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-cfg",
    filename="generator_lora.pt",
)
print(lora_path)

LongLiveの推論パスとして利用する際は、sampling_steps: 50、guidance_scale: 1.0、lora_weight_scale: 1.0 といった設定を用いることが推奨されています。

手元で動かせるか

公開元の配布形式は safetensors です。

ライセンス apache-2.0(商用利用可): 商用利用を含めて自由に利用・改変・再配布できます。再配布時にライセンス文と変更点の告知が必要で、特許の許諾条項があります。

配布形式とライセンス表記から当サイトのコードが組み立てた判定です。ライセンスの要約は法的助言ではないため、実際の利用にあたっては公開元の原文を確認してください。

当サイトでの実測

モデルカードの作例ではなく、当サイトが生成したものです。当サイトのサーバー(GPUなし)では動画のモデルを動かせないため、クラウドの GPU を借りて生成しました(条件は下記)。測り方と全モデルの一覧は観測データにまとめています。

当サイトで生成した動画(クラウドのGPU)

Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-cfg(apache-2.0 ライセンス)は単独では動かない LoRA なので、元のモデル Wan-AI/Wan2.2-TI2V-5B-Diffusers(apache-2.0 ライセンス)に、配布元が勧める組み合わせ(LongLive-Plug-Wan2.2-TI2V-5B-few-step を重み 1.0、LongLive-Plug-Wan2.2-TI2V-5B-cfg を重み 0.5)で足し込み、4ステップ・ガイダンススケール 1.0 で生成しました。サンプラーの設定は配布元の推論コード(LongLive-Plug inference.py)に合わせ、UniPC 法・shift 5.0(diffusers の UniPCMultistepScheduler)にしています。配布元はこの LoRA を LongLive-Plug-Wan2.2-TI2V-5B-few-step と組み合わせて使うよう案内しているため、両方を足し込んでおり、同じ動画をもう一方の記事にも載せています。動かしたのは diffusers 0.40.0 の WanPipeline で、配布元のコードは使っていません。LoRA の足し込みは当サイトのコードで行いました(元の Wan の層の名前を diffusers の層の名前に対応させ、各重みに 重み×(alpha/r)×BA を足す)。この対応のさせ方は配布元が文書にしているものではなく当サイトの解釈で、確かめたのは全ての層の名前と形が一致したことだけです。

条件: Modal の NVIDIA L40S(VRAM 44GB)・重みは BF16(VAE は FP32)・1280×704ピクセル・121コマ(24fps、約5.04秒)・シード 0・人物・ロゴ・文字を含まない固定のプロンプト2つ・2026-10-02生成。選別はしておらず、いずれも1回目の出力です。ただし当サイトのサーバーのアップロードの上限に収めるため、H.264 で圧縮し直しています(1本 12.0MB 以下)。細部は元の出力より甘くなることがあります。

プロンプト: 「a small red paper boat drifting down a clear forest stream, sunlight flickering through the leaves, gentle ripples, realistic」(日本語訳: 澄んだ森の小川を流れていく小さな赤い紙の舟、木の葉の間からちらつく日の光、穏やかなさざ波、写実的)

LoRA なし(元のモデル・同じ4ステップ)・生成に51秒

LoRA あり・生成に52秒

プロンプト: 「time-lapse of white clouds rolling over snowy mountain peaks at sunrise, golden light, wide shot」(日本語訳: 日の出の雪山の峰を流れていく白い雲のタイムラプス、金色の光、引きの構図)

LoRA なし(元のモデル・同じ4ステップ)・生成に52秒

LoRA あり・生成に52秒

項目 結果
動画の本体を読み込んだ直後のVRAM(BF16。テキストエンコーダは解放済み) 12.0GB
生成中の最大のVRAM 32.3GB

生成時間はクラウドの NVIDIA L40S でのもので、手元の GPU の目安ではありません。テキストエンコーダは先に動かして解放してから動画の本体を読み込んでいるので、上の VRAM には含まれません。「LoRA なし」の動画は、比べるために同じ4ステップにそろえたもので、元のモデルの通常の設定(より多いステップ数とガイダンス)ではありません。元のモデルの実力を示すものではありません。

環境構築・ダウンロード・実行の手順(当サイトで実行したもの)

当サイトでは、ダウンロードと生成を Modal の2つのコンテナに分けました。どちらも Debian(slim)・Python 3.12 のイメージです。ダウンロードのコンテナは、Hugging Face から決まったファイル(設定・safetensors の重み・トークナイザ)だけを版を固定して取ってきます。生成のコンテナはネットワークを遮断しています。入れたパッケージは次のとおりです。

# 1. ダウンロード用の実行環境を作る(GPU なし)
python -m pip install huggingface_hub

# 2. 生成用の実行環境を作る(GPU あり・ネットワークなし)
python -m pip install torch diffusers transformers accelerate safetensors sentencepiece ftfy imageio-ffmpeg numpy

そのうえで、次のスクリプト(video_remote.py、当サイトが書いたもの。全文)の download(job) をダウンロードのコンテナで、run(job) を生成のコンテナで呼びました。ファイルの置き場所は /models/<dir> です。

"""
Modal のコンテナの中で動く、動画生成モデルの生成(2026-10-01、ユーザーの判断)。lmw/lab/gpu_modal.py の
video_download / video_run から呼ばれる。**標準ライブラリだけを先頭で読み**、huggingface_hub・torch・diffusers は
各関数の中で読む(コンテナごとに入っているものが違う)。

【配布元のコードは動かさない】diffusers・transformers の公開パッケージだけで読み込む。重みは safetensors だけで、
Hugging Face のリポジトリのコードを読み込む設定は使わない。LoRA は当サイトのコード(merge_lora)が、名前の対応表と
形を照合しながら元の重みに足し込む(配布元の読み込み処理は使わない)。

【2つのコンテナに分ける】(lmw/lab/remote_code_remote.py と同じ)
- download: GPU の無いコンテナ・ネットワークあり。Hugging Face から、版を固定して決まったファイルだけを落とす。
- run: GPU のコンテナ・**ネットワーク遮断**・Modal の機能も使えない・Volume は読み取り専用。

【VRAM を節約する順番】テキストエンコーダ(UMT5-XXL は 11GB)でプロンプトを先に全部埋め込みにしてから捨て、
その後で動画の本体(transformer)と VAE を読む。2つを同時に GPU に載せないので、安い GPU に収まる。

【結果の返し方】run は生成器で、動画のバイト列を CHUNK_BYTES ずつ(kind: "chunk")、最後に記録(kind: "record")を返す。
Modal は 2MiB を超える戻り値を自分の API で預けるが、生成のコンテナは Modal の機能を遮断している
(restrict_modal_access)ので断られる(2026-10-02、14B の4本を作り終えた後に 401 で全て失った)。1回の戻り値を小さく保つ。

【費用を抑える】同じ読み込みのまま、LoRA を足す前(元のモデル)と足した後を続けて生成する(読み込みは1回)。
動画は WordPress のアップロードの上限に収まるよう H.264 で圧縮する(MAX_BYTES を超えたら画質を下げて作り直す)。

記事執筆用の LLM は呼ばない。
"""
import hashlib
import json
import os
import platform
import subprocess
import time
from datetime import datetime, timezone

MOUNT = "/models"
PYTHON_VERSION = "3.12"
DOWNLOAD_PACKAGES = ("huggingface_hub",)
# diffusers の Wan のパイプラインが使うもの(ftfy はプロンプトの整形、imageio-ffmpeg は H.264 の書き出しに使う)
RUN_PACKAGES = ("torch", "diffusers", "transformers", "accelerate", "safetensors", "sentencepiece", "ftfy",
                "imageio-ffmpeg", "numpy")
# 落としてよいファイル(pickle の .bin / .pt / .ckpt は落とさない)
ALLOWED_EXTS = (".json", ".safetensors", ".model", ".txt")

# 元の Wan の層の名前(LoRA が使う)→ diffusers の WanTransformer3DModel の層の名前。
# diffusers の変換スクリプト(scripts/convert_wan_to_diffusers.py)と同じ対応
WAN_LORA_MODULES = {
    "self_attn.q": "attn1.to_q", "self_attn.k": "attn1.to_k", "self_attn.v": "attn1.to_v",
    "self_attn.o": "attn1.to_out.0",
    "cross_attn.q": "attn2.to_q", "cross_attn.k": "attn2.to_k", "cross_attn.v": "attn2.to_v",
    "cross_attn.o": "attn2.to_out.0",
    "ffn.0": "ffn.net.0.proj", "ffn.2": "ffn.net.2",
}
# 1回に返すバイト数(Modal が自分の API に預けずにそのまま返せる 2MiB より十分小さく)
CHUNK_BYTES = 1024 * 1024
LORA_PREFIXES = ("base_model.model.", "diffusion_model.", "model.")
# 生成に使うデバイス。Modal では常に cuda(当サイトの評価で、小さなモデルを CPU で通すときだけ cpu にする)
DEVICE = "cuda"

def installed_packages() -> list[str]:
    from importlib import metadata
    seen = {}
    for dist in metadata.distributions():
        name = dist.metadata["Name"]
        if name and name.lower() not in seen:
            seen[name.lower()] = f"{name}=={dist.version}"
    return sorted(seen.values(), key=str.lower)

def _log(t0: float, message: str) -> None:
    print(f" {message}", flush=True)

def _sha256(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()

def download(job: dict) -> dict:
    """
    job: {"dir", "base": {"repo", "revision", "files"}, "loras": [{"repo", "revision", "file"}]}。
    元のモデルは <dir>/base、LoRA は <dir>/lora<i>/ に置く。戻り値: {"bytes", "seconds", "packages", "lora_sha256"}。
    """
    from huggingface_hub import hf_hub_download
    t0 = time.time()
    base_dir = os.path.join(MOUNT, job["dir"])
    total = 0
    targets = [(job["base"]["repo"], job["base"]["revision"], name, os.path.join(base_dir, "base"))
               for name in job["base"]["files"]]
    targets += [(lora["repo"], lora["revision"], lora["file"], os.path.join(base_dir, f"lora{i}"))
                for i, lora in enumerate(job.get("loras") or [])]
    for repo, revision, name, dest in targets:
        if not name.endswith(ALLOWED_EXTS) or ".." in name.split("/"):
            raise ValueError(f"落とさない種類のファイルです: {name}")
        path = hf_hub_download(repo, name, revision=revision, local_dir=dest)
        total += os.path.getsize(path)
    lora_sha = [_sha256(os.path.join(base_dir, f"lora{i}", lora["file"]))
                for i, lora in enumerate(job.get("loras") or [])]
    seconds = round(time.time() - t0)
    _log(t0, f"ダウンロード {total / 1024 ** 3:.1f}GB・{seconds}秒")
    return {"bytes": int(total), "seconds": int(seconds), "packages": installed_packages(), "lora_sha256": lora_sha}

def _lora_pairs(state: dict) -> dict:
    """{元の層の名前: {"A", "B", "alpha"?}}。名前の頭(base_model.model. 等)を外し、A と B の揃わない層は断る。"""
    pairs: dict = {}
    for key, tensor in state.items():
        name = key
        for prefix in LORA_PREFIXES:
            if name.startswith(prefix):
                name = name[len(prefix):]
                break
        if name.endswith(".lora_A.weight"):
            pairs.setdefault(name[:-len(".lora_A.weight")], {})["A"] = tensor
        elif name.endswith(".lora_B.weight"):
            pairs.setdefault(name[:-len(".lora_B.weight")], {})["B"] = tensor
        elif name.endswith(".alpha"):
            pairs.setdefault(name[:-len(".alpha")], {})["alpha"] = float(tensor.item())
        else:
            raise ValueError(f"LoRA の重みではない名前です: {key}")
    broken = [n for n, p in pairs.items() if "A" not in p or "B" not in p]
    if broken:
        raise ValueError(f"A と B が揃わない層があります: {broken[:5]}")
    return pairs

def diffusers_module_name(name: str) -> str:
    """blocks.12.self_attn.q → blocks.12.attn1.to_q。対応表に無い層は断る(推測で当てない)。"""
    parts = name.split(".")
    if len(parts) < 4 or parts[0] != "blocks" or not parts[1].isdigit():
        raise ValueError(f"対応表に無い層です: {name}")
    tail = ".".join(parts[2:])
    if tail not in WAN_LORA_MODULES:
        raise ValueError(f"対応表に無い層です: {name}")
    return f"blocks.{parts[1]}.{WAN_LORA_MODULES[tail]}"

def merge_lora(transformer, path: str, weight: float, alpha: float | None, rank: int | None) -> dict:
    """
    LoRA を元の重みに足し込む: W ← W + weight × (alpha / r) × (B @ A)。alpha と r は配布元の adapter_config.json の値を
    ホスト側が渡す(重みのファイルに alpha があればそちらを使う)。足し込みは float32 で計算してから元の型に戻す。
    全ての層が対応表で見つかり、形が一致することを確かめる(1つでも外れたら断る)。
    """
    import torch
    from safetensors.torch import load_file
    pairs = _lora_pairs(load_file(path))
    merged = 0
    for name, p in sorted(pairs.items()):
        module = transformer.get_submodule(diffusers_module_name(name))
        a, b = p["A"].float(), p["B"].float()
        r = a.shape[0]
        if rank and r != rank:
            raise ValueError(f"{name}: ランク {r} が adapter_config.json の {rank} と違います")
        scale = weight * ((p.get("alpha") or alpha or r) / r)
        w = module.weight
        delta = (b.to(w.device) @ a.to(w.device)) * scale
        if tuple(delta.shape) != tuple(w.shape):
            raise ValueError(f"{name}: 形 {tuple(delta.shape)} が元の重み {tuple(w.shape)} と違います")
        with torch.no_grad():
            w.copy_((w.float() + delta).to(w.dtype))
        merged += 1
    return {"modules": merged, "weight": weight, "scale": round(weight * ((alpha or rank or 1) / (rank or 1)), 6)}

def encode_mp4(frames, fps: int, path: str, max_bytes: int) -> dict:
    """
    0〜1 の RGB のコマ(F, H, W, 3)を H.264(yuv420p)の mp4 にする。max_bytes を超えたら、ビットレートの上限を
    下げて作り直す(3回まで)。戻り値: 実際に使った ffmpeg の引数と大きさ。
    """
    import imageio_ffmpeg
    import numpy as np
    pixels = (np.clip(frames, 0.0, 1.0) * 255).round().astype(np.uint8)
    count, height, width = pixels.shape[0], pixels.shape[1], pixels.shape[2]
    duration = count / fps
    # 上限の大きさの 85% に収まるビットレート(音声なし)
    kbps = int(max_bytes * 8 * 0.85 / duration / 1000)
    for attempt, crf in enumerate((18, 22, 26)):
        rate = int(kbps * (0.8 ** attempt))
        args = ["-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{width}x{height}", "-r", str(fps), "-i", "-",
                "-an", "-c:v", "libx264", "-preset", "slow", "-crf", str(crf), "-maxrate", f"{rate}k",
                "-bufsize", f"{rate * 2}k", "-pix_fmt", "yuv420p", "-movflags", "+faststart", path]
        proc = subprocess.run([imageio_ffmpeg.get_ffmpeg_exe(), *args], input=pixels.tobytes(),
                              capture_output=True, timeout=600)
        if proc.returncode != 0:
            raise RuntimeError(f"ffmpeg が失敗しました: {proc.stderr.decode(errors='replace')[-400:]}")
        size = os.path.getsize(path)
        if size <= max_bytes:
            return {"ffmpeg_args": ["ffmpeg", *args[:-1], os.path.basename(path)], "bytes": size,
                    "frames": int(count), "width": int(width), "height": int(height), "fps": int(fps),
                    "duration": round(duration, 2)}
    raise ValueError(f"動画を {max_bytes} バイト以下に圧縮できませんでした")

def run(job: dict):
    """
    job: {"dir", "pipeline", "settings": {height, width, num_frames, fps, steps, guidance, flow_shift?},
          "seed", "prompts": [{"id", "en"}], "loras": [{"file", "weight", "alpha", "rank"}], "compare_base",
          "max_bytes"}。
    生成器: {"kind": "chunk", "id", "index", "data": bytes} を動画ごとに順に返し、最後に
    {"kind": "record", ...記録の一部(秒数・VRAM・版。動画の中身は含まない)} を返す。
    """
    import diffusers
    import numpy as np
    import torch
    import transformers
    os.environ["HF_HUB_OFFLINE"] = "1"
    os.environ["TRANSFORMERS_OFFLINE"] = "1"
    os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
    t0 = time.time()
    base_dir = os.path.join(MOUNT, job["dir"])
    model_dir = os.path.join(base_dir, "base")
    settings = job["settings"]
    guidance = float(settings["guidance"])
    cuda = DEVICE == "cuda"
    gpu = torch.cuda.get_device_name(0) if cuda else "cpu"
    _log(t0, f"GPU: {gpu}・diffusers {diffusers.__version__}・torch {torch.__version__}")
    record = {"gpu": gpu, "vram_total": int(torch.cuda.get_device_properties(0).total_memory) if cuda else 0,
              "diffusers": str(diffusers.__version__), "transformers": str(transformers.__version__),
              "torch": str(torch.__version__), "dtype": "bfloat16", "vae_dtype": "float32",
              "packages": installed_packages(), "python": platform.python_version(),
              "measured_at": datetime.now(timezone.utc).isoformat()}
    pipeline_cls = getattr(diffusers, job["pipeline"])

    # 1. プロンプトを埋め込みにする(テキストエンコーダだけを GPU に載せ、終わったら捨てる)
    with open(os.path.join(model_dir, "model_index.json"), encoding="utf-8") as f:
        index = json.load(f)
    skip_models = {name: None for name, spec in index.items()
                   if isinstance(spec, list) and name not in ("text_encoder", "tokenizer", "scheduler")}
    text_pipe = pipeline_cls.from_pretrained(model_dir, torch_dtype=torch.bfloat16, use_safetensors=True,
                                             local_files_only=True, **skip_models)
    text_pipe.text_encoder.to(DEVICE)
    embeds = {}
    with torch.no_grad():
        for p in job["prompts"]:
            # max_sequence_length はパイプラインの __call__ の既定(512。元の Wan も 512)に合わせる。
            # encode_prompt の既定は 226 で、そのままだと標準の呼び出しと違う埋め込みになる
            pos, neg = text_pipe.encode_prompt(prompt=p["en"], negative_prompt="", device=DEVICE,
                                               do_classifier_free_guidance=guidance > 1.0,
                                               max_sequence_length=int(settings.get("max_sequence_length", 512)))
            embeds[p["id"]] = (pos.cpu(), None if neg is None else neg.cpu())
    del text_pipe
    if cuda:
        torch.cuda.empty_cache()
    record["encode_sec"] = round(time.time() - t0)

    # 2. 本体と VAE を読む(VAE は float32。diffusers の Wan の例と同じ)
    vae_cls = getattr(diffusers, index["vae"][1])
    vae = vae_cls.from_pretrained(model_dir, subfolder="vae", torch_dtype=torch.float32, use_safetensors=True,
                                  local_files_only=True)
    pipe = pipeline_cls.from_pretrained(model_dir, vae=vae, text_encoder=None, tokenizer=None,
                                        torch_dtype=torch.bfloat16, use_safetensors=True, local_files_only=True)
    if settings.get("flow_shift") is not None:
        pipe.scheduler = type(pipe.scheduler).from_config(pipe.scheduler.config, flow_shift=float(settings["flow_shift"]))
    record["scheduler"] = {"class": type(pipe.scheduler).__name__,
                           **{k: v for k, v in dict(pipe.scheduler.config).items()
                              if k in ("flow_shift", "use_flow_sigmas", "prediction_type", "solver_order")}}
    pipe.to(DEVICE)
    if cuda:
        torch.cuda.synchronize()
    record["vram_loaded"] = int(torch.cuda.memory_allocated()) if cuda else 0
    record["load_sec"] = round(time.time() - t0)
    _log(t0, f"読み込み: VRAM {record['vram_loaded'] / 1024 ** 3:.1f}GB・{record['load_sec']}秒")
    if cuda:
        torch.cuda.reset_peak_memory_stats()

    def generate(group: str) -> list[dict]:
        out = []
        for p in job["prompts"]:
            pos, neg = embeds[p["id"]]
            started = time.time()
            generator = torch.Generator("cpu").manual_seed(int(job["seed"]))
            frames = pipe(prompt_embeds=pos.to(DEVICE),
                          negative_prompt_embeds=None if neg is None else neg.to(DEVICE),
                          height=int(settings["height"]), width=int(settings["width"]),
                          num_frames=int(settings["num_frames"]), num_inference_steps=int(settings["steps"]),
                          guidance_scale=guidance, generator=generator, output_type="np").frames[0]
            if cuda:
                torch.cuda.synchronize()
            seconds = time.time() - started
            frames = np.asarray(frames, dtype=np.float32)
            if not np.isfinite(frames).all():
                raise FloatingPointError("生成したコマに数でない値があります")
            # 処理の誤り(真っ黒・一色の動画)を取り込み役の機械の検査(lmw/lab/sanity.py)が見つけるための値
            stats = {"pixel_mean": round(float(frames.mean()) * 255, 2), "pixel_std": round(float(frames.std()) * 255, 2)}
            path = os.path.join("/tmp", f"{group}-{p['id']}.mp4")
            encoded = encode_mp4(frames, int(settings["fps"]), path, int(job["max_bytes"]))
            with open(path, "rb") as f:
                raw = f.read()
            blobs[f"{group}-{p['id']}"] = raw
            out.append({"id": f"{group}-{p['id']}", "prompt_id": p["id"], "group": group,
                        "seconds": round(seconds, 1), **encoded, **stats,
                        "sha256": hashlib.sha256(raw).hexdigest()})
            _log(t0, f"{group}/{p['id']}: {encoded['frames']}コマ・生成 {seconds:.0f}秒・{encoded['bytes'] / 1e6:.2f}MB")
        return out

    videos = []
    blobs: dict[str, bytes] = {}
    loras = job.get("loras") or []
    # 3. LoRA を足す前の元のモデル(比べるため。同じプロンプト・シード・ステップ数)
    if loras and job.get("compare_base"):
        videos += generate("base")
    # 4. LoRA を足し込んでから生成する
    merges = []
    for i, lora in enumerate(loras):
        path = os.path.join(base_dir, f"lora{i}", lora["file"])
        merges.append(merge_lora(pipe.transformer, path, float(lora["weight"]), lora.get("alpha"), lora.get("rank")))
        _log(t0, f"LoRA {i}: {merges[-1]['modules']}層を足し込みました(重み {lora['weight']})")
    if merges:
        record["merges"] = merges
    videos += generate("lora" if loras else "model")
    record["vram_peak"] = int(torch.cuda.max_memory_allocated()) if cuda else 0
    record["videos"] = videos
    for vid, raw in blobs.items():
        for index in range(0, len(raw), CHUNK_BYTES):
            yield {"kind": "chunk", "id": vid, "index": index // CHUNK_BYTES, "data": raw[index:index + CHUNK_BYTES]}
    yield {"kind": "record", **plain(record)}

def plain(value):
    """JSON に通して、標準の型だけにする(ホストには torch も diffusers も無い)。"""
    return json.loads(json.dumps(value, default=str))

download に渡した job(実際に使った値):

{
  "dir": "video-1790915190",
  "base": {
    "repo": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
    "revision": "b8fff7315c768468a5333511427288870b2e9635",
    "files": [
      "model_index.json",
      "scheduler/scheduler_config.json",
      "text_encoder/config.json",
      "text_encoder/model-00001-of-00003.safetensors",
      "text_encoder/model-00002-of-00003.safetensors",
      "text_encoder/model-00003-of-00003.safetensors",
      "text_encoder/model.safetensors.index.json",
      "tokenizer/special_tokens_map.json",
      "tokenizer/spiece.model",
      "tokenizer/tokenizer.json",
      "tokenizer/tokenizer_config.json",
      "transformer/config.json",
      "transformer/diffusion_pytorch_model-00001-of-00005.safetensors",
      "transformer/diffusion_pytorch_model-00002-of-00005.safetensors",
      "transformer/diffusion_pytorch_model-00003-of-00005.safetensors",
      "transformer/diffusion_pytorch_model-00004-of-00005.safetensors",
      "transformer/diffusion_pytorch_model-00005-of-00005.safetensors",
      "transformer/diffusion_pytorch_model.safetensors.index.json",
      "vae/config.json",
      "vae/diffusion_pytorch_model.safetensors"
    ]
  },
  "loras": [
    {
      "repo": "Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-few-step",
      "revision": "38a6ec4e8f5b644749d3072bfe1ac383d00951c0",
      "file": "adapter_model.safetensors"
    },
    {
      "repo": "Efficient-Large-Model/LongLive-Plug-Wan2.2-TI2V-5B-cfg",
      "revision": "fa1f9280778aa27c8038fe06e29fb5f2ed6be451",
      "file": "adapter_model.safetensors"
    }
  ]
}

run に渡した job(実際に使った値):

{
  "dir": "video-1790915190",
  "pipeline": "WanPipeline",
  "settings": {
    "height": 704,
    "width": 1280,
    "num_frames": 121,
    "fps": 24,
    "steps": 4,
    "guidance": 1.0,
    "flow_shift": 5.0
  },
  "seed": 0,
  "prompts": [
    {
      "id": "boat",
      "en": "a small red paper boat drifting down a clear forest stream, sunlight flickering through the leaves, gentle ripples, realistic"
    },
    {
      "id": "clouds",
      "en": "time-lapse of white clouds rolling over snowy mountain peaks at sunrise, golden light, wide shot"
    }
  ],
  "loras": [
    {
      "file": "adapter_model.safetensors",
      "weight": 1.0,
      "alpha": 128,
      "rank": 128
    },
    {
      "file": "adapter_model.safetensors",
      "weight": 0.5,
      "alpha": 64,
      "rank": 64
    }
  ],
  "compare_base": true,
  "max_bytes": 12000000
}
生成のコンテナに入っていたパッケージの版(全文)
accelerate==1.15.0 aiohappyeyeballs==2.6.1 aiohttp==3.12.7 aiosignal==1.3.2 annotated-doc==0.0.5 anyio==4.15.1 attrs==25.3.0 cbor2==5.7.0 certifi==2026.7.22 charset-normalizer==3.5.2 click==8.5.0 cuda-bindings==13.4.3 cuda-pathfinder==1.8.2 cuda-toolkit==13.0.3.0 diffusers==0.40.0 filelock==4.0.9 frozenlist==1.6.0 fsspec==2026.9.0 ftfy==6.3.1 grpclib==0.4.8 h11==0.16.0 h2==4.2.0 hf-xet==1.6.0 hpack==4.1.0 httpcore==1.0.9 httpx==0.28.1 huggingface_hub==1.33.0 hyperframe==6.1.0 idna==3.20 imageio-ffmpeg==0.6.0 importlib_metadata==9.0.1 Jinja2==3.1.6 markdown-it-py==4.2.0 MarkupSafe==3.0.3 mdurl==0.1.2 mpmath==1.3.0 multidict==6.4.4 networkx==3.7 numpy==2.5.3 nvidia-cublas==13.1.1.3 nvidia-cuda-cupti==13.0.85 nvidia-cuda-nvrtc==13.0.88 nvidia-cuda-runtime==13.0.96 nvidia-cudnn-cu13==9.24.0.43 nvidia-cufft==12.0.0.61 nvidia-cufile==1.15.1.6 nvidia-curand==10.4.0.35 nvidia-cusolver==12.0.4.66 nvidia-cusparse==12.6.3.3 nvidia-cusparselt-cu13==0.8.1 nvidia-nccl-cu13==2.30.7 nvidia-nvjitlink==13.4.92 nvidia-nvshmem-cu13==3.4.5 nvidia-nvtx==13.0.85 packaging==26.3 pillow==12.3.0 pip==25.1.1 propcache==0.3.1 protobuf==6.31.1 psutil==7.2.2 Pygments==2.21.0 PyYAML==6.0.3 regex==2026.9.29 requests==2.34.2 rich==15.0.0 safetensors==0.8.0 sentencepiece==0.2.2 setuptools==84.0.0 shellingham==1.5.4 sympy==1.14.0 tokenizers==0.23.2 torch==2.14.1 tqdm==4.70.1 transformers==5.18.0 triton==3.8.0 typer==0.27.2 typing_extensions==4.16.0 urllib3==2.8.0 uv==0.7.19 wcwidth==0.9.1 wheel==0.45.1 yarl==1.20.0 zipp==4.1.0
ダウンロードのコンテナに入っていたパッケージの版(全文)
aiohappyeyeballs==2.6.1 aiohttp==3.12.7 aiosignal==1.3.2 anyio==4.15.1 attrs==25.3.0 cbor2==5.7.0 certifi==2025.4.26 click==8.5.0 filelock==4.0.7 frozenlist==1.6.0 fsspec==2026.9.0 grpclib==0.4.8 h11==0.16.0 h2==4.2.0 hf-xet==1.6.0 hpack==4.1.0 httpcore2==2.13.1 httpx2==2.13.1 huggingface_hub==2.0.0 hyperframe==6.1.0 idna==3.20 multidict==6.4.4 packaging==26.3 pip==25.1.1 propcache==0.3.1 protobuf==6.31.1 PyYAML==6.0.3 tqdm==4.70.1 truststore==0.10.4 typing_extensions==4.16.0 uv==0.7.19 wheel==0.45.1 yarl==1.20.0

関連記事

次に読むなら

出典

更新履歴

  • 2026-09-30: タイトルを、記事で分かる内容(必要VRAM・配布ファイル等)を示す形に変更しました。
  • 2026-10-02: 当サイトでの実測(当サイトで生成した動画)を追加しました。
  • 2026-10-02: タイトルを、記事で分かる内容(必要VRAM・配布ファイル等)を示す形に変更しました。