OpenAlchemyOpenAlchemy
← ブログ
エンジニアリング2026年10月18日約12分

動画スタックの中身 ── ComfyUI ワーカーで動かす Wan 2.2 と MiniMax H3

/v1/videos のリクエストが 32 GB GPU 上の ComfyUI グラフになるまで ── チェックポイント、フレーム数の計算、蒸留 LoRA、Wan 2.2 MoE がスローモーションになった理由とそれを直した 3 段サンプラー、そして H3 のオーディオガイドが実際に何をするのか。

動画 API は minimax-h3・minimax-h3-ref・wan2.2-s2v・wan2.2-i2v・wan2.2-animate・latentsync-1.6 の 6 モデルを提供しており、どれも自前のスケジューラの背後で、32 GB のコンシューマ GPU(RTX 5090)1 枚の上の ComfyUI グラフとして動いています。この記事はエンジニアリングの視点から、リクエストがどうグラフになるのか、各モデルに何が必要なのか、そして途中で間違えた 3 つのことを書きます。

リクエストからグラフへ

client ── POST /v1/videos ──▶ avionics (API gateway: auth, validation, credit hold)
                                   │  JSON + base64 inputs
                                   ▼
                             engine-server (Go, scheduler)
                                   │  NATS: job → one worker
                                   ▼
                             engine-agent (Go, on the GPU host)
                                   │  1. fetch inputs over HTTPS (signed URLs)
                                   │  2. POST /upload/image  → ComfyUI input/
                                   │  3. POST /prompt        → API-format graph
                                   │  4. watch the progress websocket
                                   ▼
                             ComfyUI (native nodes only)
                                   │  SaveVideo → mp4
                                   ▼
                    agent uploads the output → avionics settles the hold

エージェントは手書きのワークフローファイルを ComfyUI に渡しません。モデルごとに Go のビルダーがあり、ネイティブノードだけで API 形式のグラフを組み立て、書き換えるノードがすべて想定どおりのクラスかを確認し(テンプレートがずれたら、間違った絵を出す前に失敗させる)、キャンバスは解像度セレクタを通さず直接指定します。モデルをスケジューラに公開するのは、プローブでノードクラスと重みファイルの存在を確認できている間だけです。ジョブごとに ComfyUI の VRAM を解放するので、1 枚のカードで H3(fp8、作業領域 約 30 GB)と Wan 系を再起動なしに行き来できます。

入力はメッセージバスを通さず、署名付き HTTPS URL でワーカーに直接届けます。キーフレームと動きの元動画を合わせると、遅延が大きく損失のある GPU ホストへの回線では、バスのオブジェクトストア経由だと期限内に運びきれないことがよくあるためです。

MiniMax H3:2 つのチェックポイントと 1 つのフレーム格子

H3 は 2 つの DiT で構成されます。fl2va(最初・最後のフレームから映像+音声)が t2v と i2v を、ref2va が ref を担当します。ref では最大 9 枚の参照画像をプロンプト内で <Picture 1>…<Picture 9> として指します。どちらも pruned・fp8 scaled で動かし、テキストエンコーダの Qwen3-VL 32B は NVFP4(AWQ)に量子化、公式の turbo LoRA(fl2v は 8 ステップ、ref は 4 ステップ)を使います。映像とステレオ音声は同じサンプリングから出てきます ── 音声 VAE が映像 VAE と並んでデコードします。

// MiniMax H3 is trained on clip lengths of 17k + 5 frames at 24 fps
// (124 ≈ 5 s ... 362 ≈ 15 s). The template turns seconds into frames:
//
//   frames = max(5, round(seconds * 24))
//   frames = frames + (5 - frames) mod 17      // snap up to 17k + 5
//
// 5 s → 124 frames, 10 s → 243 frames. A "still" keyframe renders the
// minimum 5-frame clip and keeps frame 2 as a PNG.

出力は 768P のみです。2K のリジェネレータは公開されていないので、2K の要求は黙って下げずに拒否します。

H3 はプロンプトに書いたことを ── 全部やる

turbo LoRA を CFG 1 で使うのでネガティブプロンプトには頼れず、しかもテキストエンコーダが強いので、プロンプトに出てきた名詞はたいてい画面に現れます。3 つの失敗から、プロンプトは「そのショットの描写」だけを書くようになりました。

否定は描かれる。「画面に文字や字幕を出さない」で字幕が出て、「ジャケットは着たまま」で、もともと着ていないキャラクターにジャケットが着せられました。いまは画面にあるものだけを書きます。

光の記述はカメラを動かす。光源を名指しする設定(「デスクランプに照らされて」)を入れると、H3 はランプを見せようと引きの画になり、クローズアップが台無しになりました。光はキーフレームがすでに持っているので、動画のプロンプトではもう書きません。

参照は最初のフレームではない。ref モードではキーフレームは <Picture 1> にすぎず、H3 はシーンを描き直し、被写体の位置を変え、ペット向けの汎用文「帽子・バンダナ・リュックを身につけてもよい」を受けて 3 つとも着せました。i2v ではキーフレームが潜在フレーム 0 になり、そのまま残ります。残った失敗は、プロンプトの動作がキーフレームと食い違うときに 1 秒ほどでハードカットが入ることでした。プロンプトを「最初のフレームからの 1 カット」で始め、フレームの状態から始まる動作を書くと、試したすべての回で消えました。

H3 に音声を渡すと ── 実際に何が起きるか

H3 への音声の入り方は 2 つあります。ref モードではノードの ref_audios 入力でクリップが <Audio 1> になります。キャラクターが話す声として使われ、サウンドトラック自体は H3 が作ります。t2v / i2v では画像から動画のノードの後ろに MiniMaxH3AddGuide をつなぎます。このノードは画像・クリップ・音声を任意の潜在フレームに固定できます。

// i2v clip with a soundtrack anchored at frame 0
"oa_first_frame": { "class_type": "LoadImage",  "inputs": { "image": "kf.png" } },
"oa_guide_audio": { "class_type": "LoadAudio",  "inputs": { "audio": "line.wav" } },
"140:131": { "class_type": "MiniMaxH3ImageToVideo",
             "inputs": { "first_frame": ["oa_first_frame", 0], "prompt": "...",
                         "width": 768, "height": 1344, ... } },
"oa_guide": { "class_type": "MiniMaxH3AddGuide",
              "inputs": { "positive": ["140:131", 0], "latent": ["140:131", 1],
                          "frame_idx": 0,
                          "audio_vae": ["140:120", 0], "audio": ["oa_guide_audio", 0] } },
"140:126": { "class_type": "BasicGuider",
             "inputs": { "conditioning": ["oa_guide", 0], ... } }

ガイドでサウンドトラックが固定されると期待していましたが、出力と入力セリフの音量エンベロープを比べると違いました。

RMS per 0.1 s (8 kHz mono), speech marked ▇

our TTS line       ▁▁▁▁▁▁▁▁▁▁▇▇▇▁▁▁▁▁▁▁▁▇▇▇▇▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁   1.0–1.3 s, 2.1–2.6 s
run 1 (guided)     ▁▁▁▁▁▁▁▁▇▇▇▇▇▁▁▁▁▁▁▁▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▁▁▇▇   follows it
run 2 (guided)     ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇▇▇▁▁▁▁▁▁▁▇▇▇▇▇▇▇▁▁▁▁   ~2.9 s, ~4.0 s
run 3 (no audio)   ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▇▇▇▇▁▁▁▁▁▁▇▇▇▇▇▇▇▇▁   ~3.0 s, ~4.0 s

ある回はガイドに沿い、次の回は音声なしの回と区別がつきません。ガイドは条件付けであって拘束ではない ── そこで、サウンドトラックのヒントとして公開し、編集では H3 自身の音声を残し、特定の録音に口を合わせる必要があるショットは、入力音声がそのままサウンドトラックになる Wan 2.2 S2V に回しています。

Wan 2.2 S2V:動きは音声から来る

S2V は音声エンコーダを持つ単一の 14B モデルです。キーフレームと波形を入れると、音声とちょうど同じ長さの 16 fps の映像が、チャンクに分けてレンダリングされます。lightx2v の 4 ステップ LoRA、ModelSamplingSD3 の shift 8、CFG 1 で動かしています(品質重視用に 20 ステップ・CFG 6 の非 turbo 経路もあります)。蒸留しても目に見える劣化がないのは、動きがサンプラーの発明ではなく、音声 ── ビートと音節に合わせた頭・唇・肩 ── から来るからです。歌うミュージックビデオが 4 ステップでも生き生きして見えるのはこのためです。

Wan 2.2 I2V:MoE がスローモーションになった理由

I2V A14B はノイズレベルで分かれた 2 つのエキスパートの混合です。高ノイズエキスパートが最初のステップで構図と動きを決め、低ノイズエキスパートが細部を仕上げます。最初のグラフは定番の 4 ステップのレシピどおり ── 両エキスパートに lightx2v LoRA、CFG 1、各 2 ステップ ── でした。「角に向かって走り出す」と指示した動物が、その場で足踏みしました。

まず疑ったのはタイミングですが問題ありませんでした。16 fps、6 秒で 96 フレーム、コンテナも 6.0 秒。圧縮されていたのは動きそのものです。蒸留済みの高ノイズエキスパートを CFG 1 で使うと、2 ステップしかなくガイダンスもないので大きな移動に踏み切れず、軌跡が穏やかな漂いに縮んでしまいます。S2V で起きなかったのは、動きの出どころが別にあるからです。

修正では、LoRA の速さを害のない場所に残し、動きが決まる場所からは外しました。

// Wan 2.2 I2V A14B: two experts, 6 steps, three KSamplerAdvanced stages
shift_raw   = ModelSamplingSD3(unet_high,               shift=5)   // no LoRA
shift_high  = ModelSamplingSD3(LoRA(unet_high, lightx2v_high), shift=5)
shift_low   = ModelSamplingSD3(LoRA(unet_low,  lightx2v_low),  shift=5)

sample_raw  = KSamplerAdvanced(shift_raw,  steps=6, cfg=3.5, start=0, end=2,
                               add_noise=enable,  leftover_noise=enable)
sample_high = KSamplerAdvanced(shift_high, steps=6, cfg=1.0, start=2, end=3,
                               add_noise=disable, leftover_noise=enable)
sample_low  = KSamplerAdvanced(shift_low,  steps=6, cfg=1.0, start=3, end=6,
                               add_noise=disable, leftover_noise=disable)

// length: WanImageToVideo renders L = 4k + 1 latent frames; we ask for
// F = seconds * 16 and drop the extra frame (ImageFromBatch) — 6 s → L 97, F 96.

同じシード・同じキーフレーム・同じプロンプトで、旧グラフの犬はその場で小走りし、3 段グラフの犬はカメラに追われながら通りを画面の外まで走っていきました。蒸留なしのステップでコストが倍になるのは 6 ステップ中 2 ステップだけで、6 秒・480×832 のショットは今も約 1 分でレンダリングされます。

MiniMax H3 t2v · 5 s · 768P
155 s
MiniMax H3 i2v + audio · 5 s · 768×1344 (end to end)
194 s
MiniMax H3 t2v · 10 s · 768P
439 s
Wan 2.2 I2V · 6 s · 480×832 · 6-step three-stage
≈ 60 s
RTX 5090(32 GB)1 枚でのレンダリング時間。H3 の行はレンダリングの実時間、強調した H3 の行は入力転送と出力アップロードを含む公開 API 経由のエンドツーエンド。 · 秒

動かし続けるために

6 つの動画モデルと音声エンジンを共有する GPU は、ありふれた理由で失敗します。そこでプラットフォームは 1 つずつ明示的に扱うようになりました。キューのジョブはそれぞれ自分の期限で投入する(1 つの遅い事前チェックがバッチの残りを飢えさせていた)、依存先が 5xx や「混雑」を返したら失敗させずにキューへ戻す、音声プールは呼び出し元の期限から 20 秒経っても要求を抱えている、または 3 分間進まないインスタンスを入れ替える、そしてモニターはライブウィンドウ内のジョブだけを数え、誰も閉じなかった行は 10 日前からのキューではなく「stale」として報告する。

GPU プロバイダー向けには Engine Desktop 0.6.25 にすべて含まれています ── ダウンロードページ からどうぞ。すべてのモデルは今日から /v1/videos で呼び出せます。

試してみる

より少ない VRAM でオープンモデルを動かす

OpenAlchemy はオープンウェイトモデルのための推論レイヤーです。ここで紹介したのと同じエンジニアリングがプラットフォームを支えています。エージェントを API に向けて、今すぐ作り始めましょう。