Stable Diffusionを用いたアニメーション生成をPythonとWeb APIで実装してみた

[Supership](https://supership.jp/)の名畑です。私の大好きな[アニメイト池袋本店](https://ex.animate.co.jp/shop/ikebukuro/)と言えばそのすぐそばには惜しまれながらも4月で営業を終えた[ハレスタ](https://haresuta.jp/)。ハレスタと言えば私の中では[0.1gの誤算](https://gosan.g1-corp.com/)。0.1gの誤算と言えば「[入場と同時に「受付で演奏を開始したら」バンギャはどんな反応をするか検証してみた](https://www.youtube.com/watch?v=RKUVEAHmW1o)」はここ最近で最も笑わせていただいた動画です。

## はじめに

前々回の記事「[Stable Diffusionでの画像生成をPythonとWeb APIで実装してみた記録](https://qiita.com/nabata/items/153ed75192004420c2e2)」と前回の記事「[Stable Diffusionにおける同一人物の別表情生成をPythonとWeb APIで試みた記録](https://qiita.com/nabata/items/d1f09a76e0ec69e34517)」に続いて、今回はStable Diffusionでアニメーション生成をしてみます。[Stability Animation](https://platform.stability.ai/docs/features/animation)で紹介されているものです。Animation SDKを用います。

「[画像生成AI「Stale Diffusion」開発元がテキストからアニメーションを自動生成できる開発者向けAIツール「Stable Animation SDK」をリリース - GIGAZINE](https://gigazine.net/news/20230512-stability-ai-stable-animation-sdk/)」ですとか、ニュースサイトでも取り上げられていましたね。

## 料金

料金について詳しくは[Animation Pricing](https://platform.stability.ai/docs/features/animation/pricing)をご覧ください。静止画像の生成とアニメーションの生成を合わせた2重の金額がかかります。お気をつけください。

**Stable Diffusion v1.5**を使用して**100フレーム**のアニメーションを生成した際の料金目安は下記となるそうです。**Cadence**はどれだけ頻繁に画像を生成するかの数値です。1なら毎フレームなので金額も上がります。

| Resolution | Cadence | Credits |
| :---: | :---: | :---: |
| 512x512 | 1 | 37.5 ($0.0375) |
| 512x512 | 2 | 33.1 ($0.0331) |
| 512x512 | 3 | 29.9 ($0.0299) |
| 512x512 | 4 | 28.3 ($0.0283) |
| 768x768 | 1 | 130 ($0.13) |
| 768x768 | 2 | 97.2 ($0.0972) |
| 768x768 | 3 | 82.8 ($0.0828) |
| 768x768 | 4 | 74.7 ($0.0747) |
| 1024x1024 | 1 | 259 ($0.259) |
| 1024x1024 | 2 | 187 ($0.187) |

## 環境設定

言語はPythonです。
私の環境はインストール済みでしたのでバージョン確認のみしました。

```shell-session
$ python --version
Python 3.10.7
```

animation sdkをインストールします。

```shell-session
$ pip install "stability-sdk[anim]"
```

動画の生成のため、ffmpegとtqdmも入れておきます。
各フレームのpng画像の書き出しのみであれば不要です。

```shell-session
$ brew install ffmpeg     
```

```shell-session
$ pip install tqdm
```

## サイバーパンクな都市を生成してみる

では実際にアニメーションの生成を開始します。

### import

まずは使用するモジュールをインポートします。**time**はファイル名に**timestamp**を入れる目的のためだけに使っています。

```Python
from stability_sdk import api
from stability_sdk.animation import AnimationArgs, Animator
from stability_sdk.utils import create_video_from_frames
from stability_sdk.api import ClassifierException, OutOfCreditsException
from tqdm import tqdm
import os
import time
```

### HOSTとAPI KEYの指定

**STABILITY\_API\_KEY**は過去の記事で取得済みのものを使います。

```Python
STABILITY_HOST = "grpc.stability.ai:443"
STABILITY_KEY = os.getenv("STABILITY_API_KEY")
context = api.Context(STABILITY_HOST, STABILITY_KEY)
```

### パラメーター

```Python
args = AnimationArgs()
args.model = "stable-diffusion-xl-beta-v2-2-2"  # モデル
args.max_frames = 72  # フレーム数
args.strength_curve = "0:(0)"  # 前の画像をどれだけ反映するか
args.diffusion_cadence_curve = "0:(8)"  # 画像生成間隔
args.cadence_interp = "film"  # 補完モード
```

せっかくなのでモデルは最新版である**Stable Diffusion XL**（**SDXL**）を指定しています。
**strength\_curve**については、今回は前の画像を引き継がない設定としてみました。**0フレーム目に0という値**を指定しています。
**diffusion\_cadence\_curve**は何フレーム毎に画像生成を行うかになります。今回の場合は8フレーム毎を指定していますが、では生成される間の7フレームの画像がどう補完されるかというと、**cadence\_interp**が適用されるようです。デフォルト(**mix**)のままだと透過切り替えのような出来栄えで見た目として好みではなかったので**film**を指定してみました。

指定可能なパラメータについて詳しくは公式サイトの[Animation Parameters](https://platform.stability.ai/docs/features/animation/parameters)をご参照ください。[Animation Handbook](https://docs.google.com/document/d/1iHcAu_5rG11guGFie8sXBXPGuM4yKzqdd13MJ_1LU8U/edit)を読むとより理解が進みそうです。カメラ位置なども含め、かなり細かい設定が可能です。

### プロンプト

[Animation Handbook](https://docs.google.com/document/d/1iHcAu_5rG11guGFie8sXBXPGuM4yKzqdd13MJ_1LU8U/edit)にあったサンプルを真似してみました。ネガティブプロンプト(除外要素)は指定していません。デフォルトで「**blurry, low resolution**」が指定されているそうです。

```Python
animation_prompts = {
    0: "A cyberpunk futuristic colourful crowded luxurious pedestrian street avenue hi-tech at morning time, blue neon lights, ray tracing, hdr, realistic shaded, extremely detailed, sharp focus, soft lighting, sunny"
}
```

日本語に訳すと「**朝の時間帯におけるサイバーパンクの未来的でカラフルで混雑した高級歩行者専用道路の大通り、ブルーのネオンライト、レイトレーシング、HDR、リアルなシェーディング、非常に詳細、鮮明な焦点、柔らかな照明、晴天**」といったところでしょうか。

### Animatorの生成

```Python
now_time = int(time.time())  # フォルダとファイルの名称に使うtimestamp

# Animatorの生成
# out_dirで指定したディレクトリに各フレーム毎のpng画像が書き出されます
animator = Animator(
    api_context=context,
    animation_prompts=animation_prompts,
    args=args,
    out_dir=f"video_{now_time}"
)
```

### レンダリング

サンプルコードを参考に例外処理を加えています。

```Python
try:
    for _ in tqdm(animator.render(), total=args.max_frames):
        pass
except ClassifierException:
    print("Animation terminated early due to NSFW classifier.")
except OutOfCreditsException as e:
    print(f"Animation terminated early, out of credits.\n{e.details}")
except Exception as e:
    print(f"Animation terminated early due to exception: {e}")
```

### mp4ファイル生成

**fps**(1秒当たりのフレーム数)は24にしておきました。

```Python
create_video_from_frames(animator.out_dir, f"video_{now_time}.mp4", fps=12)
```

## 結果

アニメーションGIFにしたものがこちらです。変換過程で画質やフレーム数削減しているため、実際より滑らかさは減じています。

8フレーム毎に別画像に切り替わります。

![video_1685238863_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/67ac3110-9d1c-7625-e6ca-b109af78a8e2.gif)


## 人物で生成してみる

ありがちですが、年齢に応じた変化的なものをアニメーションにしてみようと思います。

先ほどのサイバーパンクを描いたコードのうちのパラメーターとプロンプトのみ変更します。

### パラメーター

```Python
args = AnimationArgs()
args.model = "stable-diffusion-xl-beta-v2-2-2"  # モデル
args.locked_seed = True  # フレーム毎のシードを固定する
args.max_frames = 72  # フレーム数
args.seed = 99999  # シード値
args.strength_curve = "0:(0)"  # 前の画像をどれだけ反映するか
args.diffusion_cadence_curve = "0:(4)"  # 画像生成間隔
args.cadence_interp = "film"  # 補完モード
```

**seed**と**locked_seed**を追加してみました。こうすることで生成される画像に統一性が生まれることを期待しました。seedについては前回の記事「[Stable Diffusionにおける同一人物の別表情生成をPythonとWeb APIで試みた記録](https://qiita.com/nabata/items/d1f09a76e0ec69e34517)」で説明していますので、よければご覧ください。

### プロンプト

8フレーム毎に20歳、25歳、30歳、35歳……と年齢を加算していったプロンプトを記載しています。

```Python
animation_prompts = {
    0: "20 years old Japanese woman",
    8: "25 years old Japanese woman",
    16: "30 years old Japanese woman",
    24: "35 years old Japanese woman",
    32: "40 years old Japanese woman",
    40: "45 years old Japanese woman",
    48: "50 years old Japanese woman",
    56: "55 years old Japanese woman",
    64: "60 years old Japanese woman",
}
```

### 結果

アニメーションGIFにしたものがこちらです。変換過程で画質やフレーム数削減しているため、実際より滑らかさは減じています。

![video_1685238529_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/1bce1484-c586-9924-7d0c-168d093b8d22.gif)


## 最後に

アニメーション自体をプロンプトで指示するのではなく、プロンプトで指示した静止画をアニメーションさせる感じに近いですかね。私の理解がまだ追いついていないだけかもしれない。かなり面白いけれど私の頭だとまだ使い所が思い浮かんでおりません。
「**After Effectsの方がやりやすいよね**」とか言わないでください。

## 宣伝

[SupershipのQiita Organization](https://qiita.com/organizations/supership-inc)を合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方は[Supership株式会社 採用サイト](https://supership.jp/recruit/)よりご確認ください。