動画生成モデルに参照画像を渡すAPIは増えたが、渡し方の設計はまだ揃っていない。そしてこの設計の差が、実装した後の使い勝手をかなり決めてしまう。
主流は二通りある。スロット方式と番号引用方式だ。
スロット方式:役割を先に決めておく
character_image、style_image、background_image のように、パラメータ名そのものが役割を持つ形。
{
"prompt": "彼女が振り返る",
"character_image": "https://.../face.jpg",
"style_image": "https://.../style.jpg"
}
実装は楽だ。バリデーションもフィールド単位で書ける。ただしプロンプト本文と参照素材が別々の世界にいるという問題が残る。「彼女」がどの画像を指すのかは、プロンプトのどこにも書かれていない。役割名で暗黙に紐づいているだけだ。
だから被写体がひとりなら破綻しないが、ふたり出てくると急に破綻する。character_image はひとつしかない。
番号引用方式:プロンプトの中から名指しする
もう一方は、参照素材を配列で渡し、プロンプト本文の中から番号で参照する形。
{
"prompt": "画像1の人物が、画像2の人物に振り返って手を振る。動画1のカメラワークで。",
"media": [
{"type": "image", "url": "https://.../person_a.jpg"},
{"type": "image", "url": "https://.../person_b.jpg"},
{"type": "video", "url": "https://.../camera_move.mp4"}
]
}
冗長に見えるが、これは参照解決をモデルの言語理解側に寄せた設計だ。効いてくるのは次の三点。
1. 被写体が複数でも破綻しない。 スロットが足りなくなるという概念がない。10枚渡して、そのうち3枚だけをプロンプトから引く、ということもできる。
2. 役割を後から決められる。 同じ画像を「主役」にも「背景の一部」にも使える。スロット方式ではAPI設計者が先に決めた役割から逃げられない。
3. 時間方向の指定ができる。 「前半は画像1、後半は画像2」がプロンプトの文として書ける。スロット方式でこれをやるには、結局パラメータをもう一段増やすことになる。
実装側で必ず踏む地雷
番号引用方式を採る場合、配列の順序がそのまま参照番号になる。つまり配列を組み立てるコードは順序を保証しなければならない。
これは言うほど簡単ではない。よくある事故は、アップロード処理を Promise.all で並列化して、完了順に配列へ push してしまうパターンだ。ローカルでは速い順が入力順と一致するので通ってしまい、本番で大きいファイルが混ざった瞬間に「画像1」が別人になる。
// 事故る
const media = []
await Promise.all(files.map(async f => {
media.push({ type: 'image', url: await upload(f) }) // 完了順
}))
// 正しい
const media = await Promise.all(
files.map(async f => ({ type: 'image', url: await upload(f) })) // 入力順
)
エラーにならず、ただ結果が違うだけなので、テストで気づきにくい。
排他ルールも配列側にある
もうひとつ。参照素材の配列と「最初のフレーム/最後のフレーム」指定は、たいてい同時に使えない。
配列を見ていればこの排他は構造的に自明なのだが、ゲートウェイが配列を7個のトップレベルフィールドに平坦化した瞬間に見えなくなる。フィールドが並列に並んでいるだけの形になり、どの組み合わせが違法なのかリクエストの形から読み取れなくなるからだ。ラップするなら送信前に排他を検証すること。失敗したジョブから学ぶには遅すぎる。
開示
筆者は wan-3.run を運営している。Alibaba の Wan 3.0 をブラウザから扱える独立系のサードパーティ製インターフェースで、Alibaba でも Alibaba Cloud でも通義万相でもない。上の番号引用方式は Wan 3.0 が採っている形で、枠ごとの上限(画像10・動画5・音声5)や排他の詳細は参照素材を番号で引く仕組みを解説したページにまとめてある。
ただし設計の話自体はモデル非依存だ。参照付き生成をラップする予定があるなら、そのAPIがスロット方式か番号引用方式かを最初に確認しておくと、後から作り直さずに済む。