DOKを使ってCogVideoXでテキストから動画を生成する

[DOK](https://www.sakura.ad.jp/koukaryoku-dok/)はコンテナー型のGPUサービスで、NVIDIA V100やH100を実行時間課金で利用できるサービスです。

[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） | さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)

今回はこのDOKを使って、[CogVideoX](https://github.com/THUDM/CogVideo)にてテキストから動画を生成してみました。

## 結果

結果は以下のようになっています。ちょっと猫の指先が怪しい時もありますが、全体として崩壊が少ないんじゃないかと思います。

一瞬、猿の手みたいになる…

![output-b8099bc0-2915-4c89-b749-09717b7e9bb7 2.mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/77666ed1-2c15-da3b-6d12-e890444c2b3e.gif)

![boy.mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f37502fb-7b93-c8a3-764f-f6168b95cd0e.gif)

![output_1 (1).mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9636e7bf-4e6a-6558-d2c9-53121fe98fe6.gif)

画像 + プロンプトの場合。写真は[写真素材：雪の降る街角で暖かい飲み物を飲む女性](https://www.photo-ac.com/main/detail/31028746/31028745,31028747)より。

![i2v_1 (3).mp4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/2eed861e-eca8-2c7f-727c-02b1a253294d.gif)


## 参考

[動画生成AI【CogVideoX】をローカルで実行する](https://zenn.dev/asap/articles/a4077e8b9ddf1a)を参考にさせてもらっています。

## とりあえず試す

学習済みのデータを使って実行してみたい方は、DOKにて新しいタスクを作成し、以下の情報を入力してください。

| 項目     | 設定                                                          |
| -------- | ------------------------------------------------------------- |
| イメージ | dok-handson.sakuracr.jp/cog-video-x                            |
| 環境変数 | PROMPT=A black-and-white tiger-striped cat is staring fixedly at a round fish tank. Inside the tank are two orange goldfish, one large and one small. The cat puts its hand into the tank and tries to catch the goldfish. The goldfish jumps out of the water and escapes the cat's hand. The goldfish escapes the cat's hand and swims around in the tank again.<br />TYPE=t2v |

PROMPTで指定したテキストに対して動画を生成します。t2vはテキストからビデオを生成することを示しています。他、IMAGEで画像を指定しつつ、TYPEをi2vにして画像から動画の生成もできます。IMAGEで動画を指定して、TYPEをv2vにすると動画から動画もできるようですが、試していません。

![FireShot Capture 268 - 高火力 DOK - タスク › 新規作成 - secure.sakura.ad.jp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f6f06631-0627-a3f4-479c-fcace1644f30.jpeg)


なお、プランはh100-80gbを指定してください。デフォルトのv100-32gbではメモリが足りないため、エラーが発生します。

## コンテナイメージの作成と登録

上記タスクで利用したDockerイメージを作成する手順は以下の通りです。ベースとして、[動画生成AI【CogVideoX】をローカルで実行する](https://zenn.dev/asap/articles/a4077e8b9ddf1a)を参考にしつつ、DOK向けに改造しています。

完成版は[goofmint/dok-cog-video-x](https://github.com/goofmint/dok-cog-video-x)にありますので、実装時の参考にしてください。

### requirements.txt の作成

DOKで実行する際に必要なライブラリを追加します。boto3は今回使っていませんが、できあがった動画を任意のオブジェクトストレージにアップロードする場合、利用できます。

```txt
transformers
accelerate
diffusers
imageio-ffmpeg
torch
sentencepiece
opencv-python
imageio
imageio-ffmpeg
boto3
```

### Dockerfile の作成

Dockerfileの内容は以下の通りです。必要なライブラリをインストールし、ファイルをコピーしています。また、 `requirements.txt` を使ってライブラリをインストールしています。

```Dockerfile
FROM nvidia/cuda:12.5.1-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive

# 必要なパッケージのインストール
# /appはアプリのディレクトリ、/opt/artifactはアウトプット先のディレクトリ
RUN apt-get update && \
    apt-get install -y \
        git \
        python3 \
        python3-pip \
      && \
    mkdir /app /opt/artifact && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt runner.py .
# 依存ライブラリのインストール
RUN pip install -r requirements.txt

# Dockerコンテナー起動時に実行するスクリプト（後で作成）
COPY docker-entrypoint.sh /
# 実行権限を付与
RUN chmod +x /docker-entrypoint.sh /

WORKDIR /
# Dockerコンテナー起動時に実行するスクリプトを指定して実行
CMD ["/bin/bash", "/docker-entrypoint.sh"]
```

### docker-entrypoint.sh の作成

`docker-entrypoint.sh` はDockerコンテナー起動時に実行するスクリプトです。ここでは環境変数をチェックして、 `runner.py` を呼び出します。以下の内容で作成します。

```bash
#!/bin/bash
 
set -ue
shopt -s nullglob

export TZ=${TZ:-Asia/Tokyo}

# アウトプット先ディレクトリ（自動付与） /opt/artifact固定です
if [ -z "${SAKURA_ARTIFACT_DIR:-}" ]; then
  echo "Environment variable SAKURA_ARTIFACT_DIR is not set" >&2
  exit 1
fi

# DOKのタスクID（自動付与）
if [ -z "${SAKURA_TASK_ID:-}" ]; then
  echo "Environment variable SAKURA_TASK_ID is not set" >&2
  exit 1
fi

# 読み上げるテキスト（環境変数で指定）
if [ -z "${PROMPT:-}" ]; then
  echo "Environment variable PROMPT is not set" >&2
  exit 1
fi

# 実行する処理
if [ -z "${TYPE:-}" ]; then
  TYPE="t2v"
fi

# S3_はすべてboto3用の環境変数です
pushd /app
  python3 runner.py \
	  --id="${SAKURA_TASK_ID}" \
	  --output="${SAKURA_ARTIFACT_DIR}" \
	  --prompt="${PROMPT}" \
    --type="${TYPE}" \
    --image="${IMAGE:-}" \
	  --s3-bucket="${S3_BUCKET:-}" \
	  --s3-endpoint="${S3_ENDPOINT:-}" \
	  --s3-secret="${S3_SECRET:-}" \
	  --s3-token="${S3_TOKEN:-}"
popd
```

### runner.py の作成

runner.py は実際に処理を行うスクリプトです。まず必要なライブラリをインポートします。

```python
from typing import Literal
import argparse

import torch
from diffusers import (
    CogVideoXPipeline,
    CogVideoXDDIMScheduler,
    CogVideoXDPMScheduler,
    CogVideoXImageToVideoPipeline,
    CogVideoXVideoToVideoPipeline,
)
from diffusers.utils import export_to_video, load_image, load_video
```

#### パラメータの取得

`docker-entrypoint.sh` から渡されたパラメータを取得します。

```python
arg_parser = argparse.ArgumentParser()

arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)
arg_parser.add_argument(
    '--prompt',
    default='',
    help='プロンプト',
)
arg_parser.add_argument(
    '--type',
    default='t2v',
    help='実行する処理',
)
arg_parser.add_argument(
    '--image',
    default='',
    help='ベースの画像（または動画）',
)

arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)
arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

args = arg_parser.parse_args()
```

### 動画生成処理を行う関数の追加

動画生成は `generate_video` という関数で行います。

```python
def generate_video(
    prompt: str,
    model_path: str,
    lora_path: str = None,
    lora_rank: int = 128,
    output_path: str = "./output.mp4",
    image_or_video_path: str = "",
    num_inference_steps: int = 50,
    guidance_scale: float = 6.0,
    num_videos_per_prompt: int = 1,
    strength: float = 0.8,
    dtype: torch.dtype = torch.bfloat16,
    generate_type: str = Literal["t2v", "i2v", "v2v"],  # i2v: image to video, v2v: video to video
    seed: int = 42,
):

    image = None
    video = None

    print(image_or_video_path)

    if generate_type == "i2v":
        pipe = CogVideoXImageToVideoPipeline.from_pretrained(model_path, torch_dtype=dtype)
        image = load_image(image=image_or_video_path)
    elif generate_type == "t2v":
        pipe = CogVideoXPipeline.from_pretrained(model_path, torch_dtype=dtype)
    else:
        pipe = CogVideoXVideoToVideoPipeline.from_pretrained(model_path, torch_dtype=dtype)
        video = load_video(image_or_video_path)

    # If you're using with lora, add this code
    if lora_path:
        pipe.load_lora_weights(lora_path, weight_name="pytorch_lora_weights.safetensors", adapter_name="test_1")
        pipe.fuse_lora(lora_scale=1 / lora_rank)

    pipe.scheduler = CogVideoXDPMScheduler.from_config(pipe.scheduler.config, timestep_spacing="trailing")

    pipe.enable_sequential_cpu_offload()
    #pipe.enable_model_cpu_offload()

    pipe.vae.enable_slicing()
    pipe.vae.enable_tiling()

    if generate_type == "i2v":
        video_generate = pipe(
            prompt=prompt,
            image=image,  # The path of the image to be used as the background of the video
            num_videos_per_prompt=num_videos_per_prompt,  # Number of videos to generate per prompt
            num_inference_steps=num_inference_steps,  # Number of inference steps
            num_frames=49,  # Number of frames to generate，changed to 49 for diffusers version `0.30.3` and after.
            use_dynamic_cfg=True,  # This id used for DPM Sechduler, for DDIM scheduler, it should be False
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),  # Set the seed for reproducibility
        ).frames[0]
    elif generate_type == "t2v":
        video_generate = pipe(
            prompt=prompt,
            num_videos_per_prompt=num_videos_per_prompt,
            num_inference_steps=num_inference_steps,
            num_frames=49,
            use_dynamic_cfg=True,
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),
        ).frames[0]
    else:
        video_generate = pipe(
            prompt=prompt,
            video=video,  # The path of the video to be used as the background of the video
            num_videos_per_prompt=num_videos_per_prompt,
            num_inference_steps=num_inference_steps,
            # num_frames=49,
            use_dynamic_cfg=True,
            strength=strength,
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),  # Set the seed for reproducibility
        ).frames[0]
    # 5. Export the generated frames to a video file. fps must be 8 for original video.
    export_to_video(video_generate, output_path, fps=8)
```

### 生成に必要な情報の準備

`generate_video` 関数に必要な情報を準備します。

```python
dtype = torch.bfloat16
generate_type = args.type # t2v: text to video, i2v: image to video, v2v: video 

if not args.image:
    image_or_video_path = args.image

lora_path = None
strength = 0.8
prompt=args.prompt

# モデルの指定
if generate_type == "t2v":
    model_path = "THUDM/CogVideoX-5b"
elif generate_type == "i2v":
    model_path = "THUDM/CogVideoX-5b-I2V"
else:
    model_path = "THUDM/CogVideoX-5b"

output_path = f'{args.output}/output-{args.id}.mp4' # 動画の出力先
```

### 動画の生成

`generate_video` 関数を呼び出して、動画を生成します。

```python
generate_video(
    prompt=prompt,
    model_path=model_path,
    lora_path=lora_path,
    lora_rank=128,
    output_path=output_path,
    image_or_video_path=image_or_video_path,
    num_inference_steps=50,
    strength=strength,
    guidance_scale=6.0,
    num_videos_per_prompt=1,
    dtype=dtype,
    generate_type=generate_type,
    seed=42,
)
```

### 全体の処理

`runner.py` の全体の処理は以下の通りです。

```python
from typing import Literal
import argparse

# PyTorchをインポート
import torch

# Diffusersライブラリから必要なモジュールをインポート
from diffusers import (
    CogVideoXPipeline,
    CogVideoXDDIMScheduler,
    CogVideoXDPMScheduler,
    CogVideoXImageToVideoPipeline,
    CogVideoXVideoToVideoPipeline,
)

from diffusers.utils import export_to_video, load_image, load_video

# コマンドライン引数を処理するためのArgumentParserを作成
arg_parser = argparse.ArgumentParser()

# 出力先ディレクトリを指定する引数
arg_parser.add_argument(
    '--output',
    default='/opt/artifact',
    help='出力先ディレクトリを指定します。',
)

# 動画生成に使うプロンプト（説明文や指示文）を指定する引数
arg_parser.add_argument(
    '--prompt',
    default='',
    help='プロンプト',
)

# 処理タイプ（テキストから動画など）を指定する引数
arg_parser.add_argument(
    '--type',
    default='t2v',
    help='実行する処理',
)

# ベースとなる画像または動画を指定する引数
arg_parser.add_argument(
    '--image',
    default='',
    help='ベースの画像（または動画）',
)

# タスクIDを指定する引数
arg_parser.add_argument(
    '--id',
    default='',
    help='タスクIDを指定します。',
)

# S3関連の設定をする引数（オプション）
arg_parser.add_argument('--s3-bucket', help='S3のバケットを指定します。')
arg_parser.add_argument('--s3-endpoint', help='S3互換エンドポイントのURLを指定します。')
arg_parser.add_argument('--s3-secret', help='S3のシークレットアクセスキーを指定します。')
arg_parser.add_argument('--s3-token', help='S3のアクセスキーIDを指定します。')

# コマンドライン引数を解析して取得
args = arg_parser.parse_args()

# 動画を生成する関数
def generate_video(
    prompt: str,  # 動画生成の指示文
    model_path: str,  # 使用するモデルのパス
    lora_path: str = None,  # LoRAモデルのパス（オプション）
    lora_rank: int = 128,  # LoRAモデルのランク（圧縮度）
    output_path: str = "./output.mp4",  # 出力する動画のパス
    image_or_video_path: str = "",  # 入力画像または動画のパス
    num_inference_steps: int = 50,  # 推論ステップ数
    guidance_scale: float = 6.0,  # ガイダンススケール（生成内容の制御）
    num_videos_per_prompt: int = 1,  # プロンプトごとに生成する動画の数
    strength: float = 0.8,  # 元画像/動画の強度（v2v用）
    dtype: torch.dtype = torch.bfloat16,  # データ型
    generate_type: str = Literal["t2v", "i2v", "v2v"],  # 処理タイプ（テキストから動画など）
    seed: int = 42,  # 乱数シード（再現性のため）
):

    # 入力画像や動画の初期化
    image = None
    video = None

    print(image_or_video_path)  # 入力パスを表示

    # 処理タイプに応じて異なるパイプラインを使用
    if generate_type == "i2v":
        pipe = CogVideoXImageToVideoPipeline.from_pretrained(model_path, torch_dtype=dtype)
        image = load_image(image=image_or_video_path)  # 画像をロード
    elif generate_type == "t2v":
        pipe = CogVideoXPipeline.from_pretrained(model_path, torch_dtype=dtype)
    else:
        pipe = CogVideoXVideoToVideoPipeline.from_pretrained(model_path, torch_dtype=dtype)
        video = load_video(image_or_video_path)  # 動画をロード

    # LoRAモデルを使用する場合の設定
    if lora_path:
        pipe.load_lora_weights(lora_path, weight_name="pytorch_lora_weights.safetensors", adapter_name="test_1")
        pipe.fuse_lora(lora_scale=1 / lora_rank)

    # スケジューラーの設定
    pipe.scheduler = CogVideoXDPMScheduler.from_config(pipe.scheduler.config, timestep_spacing="trailing")

    # メモリを効率的に使用する設定
    pipe.enable_sequential_cpu_offload()
    pipe.vae.enable_slicing()
    pipe.vae.enable_tiling()

    # 動画を生成
    if generate_type == "i2v":
        video_generate = pipe(
            prompt=prompt,
            image=image,
            num_videos_per_prompt=num_videos_per_prompt,
            num_inference_steps=num_inference_steps,
            num_frames=49,  # フレーム数
            use_dynamic_cfg=True,
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),
        ).frames[0]
    elif generate_type == "t2v":
        video_generate = pipe(
            prompt=prompt,
            num_videos_per_prompt=num_videos_per_prompt,
            num_inference_steps=num_inference_steps,
            num_frames=49,
            use_dynamic_cfg=True,
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),
        ).frames[0]
    else:
        video_generate = pipe(
            prompt=prompt,
            video=video,
            num_videos_per_prompt=num_videos_per_prompt,
            num_inference_steps=num_inference_steps,
            use_dynamic_cfg=True,
            strength=strength,
            guidance_scale=guidance_scale,
            generator=torch.Generator().manual_seed(seed),
        ).frames[0]

    # 生成されたフレームを動画ファイルにエクスポート
    export_to_video(video_generate, output_path, fps=8)

# データ型と処理タイプの設定
dtype = torch.bfloat16
generate_type = args.type  # 't2v': テキストから動画, 'i2v': 画像から動画, 'v2v': 動画から動画

# 入力画像または動画のパスを取得
if not args.image:
    image_or_video_path = args.image

# モデルやプロンプトの設定
lora_path = None
strength = 0.8
prompt = args.prompt

# 処理タイプに応じて異なるモデルを使用
if generate_type == "t2v":
    model_path = "THUDM/CogVideoX-5b"
elif generate_type == "i2v":
    model_path = "THUDM/CogVideoX-5b-I2V"
else:
    model_path = "THUDM/CogVideoX-5b"

# 出力ファイルパスを設定
output_path = f'{args.output}/output-{args.id}.mp4'

# 動画生成の開始
print(f"Generating video")
generate_video(
    prompt=prompt,
    model_path=model_path,
    lora_path=lora_path,
    lora_rank=128,
    output_path=output_path,
    image_or_video_path=image_or_video_path,
    num_inference_steps=50,
    strength=strength,
    guidance_scale=6.0,
    num_videos_per_prompt=1,
    dtype=dtype,
    generate_type=generate_type,
    seed=42,
)
```

### Dockerイメージのビルド

上記の内容で、Dockerイメージをビルドします。Linux環境などで行います。

#### コンテナレジストリの用意

Dockerイメージを登録するコンテナレジストリを作成します。さくらのクラウドではLAB機能で、コンテナレジストリを提供しています。さくらのクラウドにログインしたら `さくらのクラウド` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e247a30c-bbc7-fb7e-136d-71ee2a795533.png)

左側のメニューの `LAB` の中にある `コンテナレジストリ` を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/25d041f2-9039-fec3-77c6-88edf5afa936.png)

`追加` を押して、コンテナレジストリを作成します。最低限、以下の入力が必要です。

| 項目                 | 設定                                                                      |
| -------------------- | ------------------------------------------------------------------------- |
| 名前                 | 分かりやすい、任意の名前を入力してください                                |
| コンテナレジストリ名 | ドメイン名に使われます。以下では、 `EXAMPLE.sakuracr.jp` として説明します |
| 公開設定             | Pullのみとします                                                          |

#### ユーザーの作成

コンテナレジストリを作成したら、作成したコンテナレジストリを一覧でダブルクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6425f493-0ce1-db62-f322-64a51867ce58.png)

詳細表示にて、ユーザータブをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/79bdc51c-ea66-8d8f-c801-8f54346cf960.png)

追加ボタンを押し、ユーザーを作成します。 `YOUR_USER_NAME` と `PASSWORD` は任意のものを指定してください。

| 項目           | 設定           |
| -------------- | -------------- |
| ユーザー名     | YOUR_USER_NAME |
| パスワード     | YOUR_PASSWORD  |
| ユーザ権限設定 | All            |

#### Dockerイメージのビルド

DockerイメージはLinuxで行います。今回はUbuntu 24.04を使っています。Dockerが使える環境であれば、Windows + WSL2でも問題ありません。macOSの場合、アーキテクチャが異なるので動かせないかも知れません（未検証です）。

`EXAMPLE.sakuracr.jp` の部分は、作成したコンテナレジストリのドメイン名に置き換えてください。また、 `cog-video-x` は任意の名前で大丈夫です（以下はその名称で読み替えてください）。

```bash
sudo docker build -t EXAMPLE.sakuracr.jp/cog-video-x:latest .
```

#### コンテナレジストリへのログイン

作成したコンテナレジストリにログインします。ログインIDとパスワードが求められるので、作成したものを入力してください。

```bash
sudo docker login EXAMPLE.sakuracr.jp
```

#### イメージのプッシュ

作成したイメージをコンテナレジストリにプッシュします。イメージサイズが大きいので、数十分かかります。

```bash
sudo docker push EXAMPLE.sakuracr.jp/cog-video-x:latest
```

## タスクを作成する

後は最初と同じようにDOKでタスクを作成、実行します。

| 項目     | 設定                                                          |
| -------- | ------------------------------------------------------------- |
| イメージ | EXAMPLE.sakuracr.jp/cog-video-x                               |
| 環境変数 | PROMPT=A black-and-white tiger-striped cat is staring fixedly at a round fish tank. Inside the tank are two orange goldfish, one large and one small. The cat puts its hand into the tank and tries to catch the goldfish. The goldfish jumps out of the water and escapes the cat's hand. The goldfish escapes the cat's hand and swims around in the tank again.<br />TYPE=t2v |

![FireShot Capture 268 - 高火力 DOK - タスク › 新規作成 - secure.sakura.ad.jp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f6f06631-0627-a3f4-479c-fcace1644f30.jpeg)

## まとめ

今回はCogVideoXを使って、DOK上で動画を作成する手順を解説しました。まずは実行できるのみ、次にDockerイメージの作成と段階的に進められるようにしています。ぜひ、試してみてください。

DOKはタスクを多数立ち上げて、後は結果を待つのみと言った使い方ができます。ぜひAI・機械学習に活用してください。

[コンテナー型GPUクラウドサービス 高火力 DOK（ドック） \| さくらインターネット](https://www.sakura.ad.jp/koukaryoku-dok/)