Livebook から Amazon の最新 AI モデル、 Amazon Nova を呼んで、文書理解、画像生成、動画生成などなどをやってみた

## はじめに

先日発表された Amazon の最新 AI 、 Amazon Nova

https://aws.amazon.com/blogs/aws/introducing-amazon-nova-frontier-intelligence-and-industry-leading-price-performance/

早速、 Elixir の Livebook から Bedrock 経由で呼んでみましょう

https://aws.amazon.com/jp/bedrock/

:::note info
2024年12月10日現在の情報です
:::

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aws/bedrock_nova.livemd

## Amazon Nova で出来ること

Amazon Nova には以下の 5 種類が存在します

- Amazon Nova Micro: 安い、速い、そこそこの精度、テキストのみ理解
- Amazon Nova Lite: 中くらいの価格、速度、精度、テキストも画像も動画も理解
- Amazon Nova Pro: お高いけど最高位の速度と精度、テキストも画像も動画も理解
- Amazon Nova Canvas: テキストと画像から画像を生成
- Amazon Nova Reel: テキストと画像から動画を生成

https://docs.aws.amazon.com/nova/latest/userguide/what-is-nova.html

どのモデルも 200 言語以上に対応していて、日本語にも対応しています（特に最適化されている 15 言語にも含まれる）

## Amazon Nova の料金

公式の価格表を英語表示にしないと出てきません

https://aws.amazon.com/bedrock/pricing/?nc1=h_ls

リージョンも現在は us-east-1 しか対応していません

| モデル | 1,000 入力トークン | 1,000 出力トークン |
|:--|:--|:--|
| Amazon Nova Micro | $0.000035 | $0.00014 |
| Amazon Nova Lite | $0.00006 | $0.00024 |
| Amazon Nova Pro | $0.0008 | $0.0032 |

| モデル | 画像解像度 | 標準品質 | プレミアム品質 |
|:--|:--|:--|:--|
| Amazon Nova Canvas | 1024 x 1024 以下 | $0.04 | $0.06 |
| Amazon Nova Canvas | 2048 x 2048 以下 | $0.06 | $0.08 |

| モデル | 動画解像度 | フレームレート | 動画生成 1 秒あたり |
|:--|:--|:--|:--|
| Amazon Nova Reel | 720p | 24 fps | $0.08 |

ちなみに同じ us-east-1 の Claude

| モデル | 1,000 入力トークン | 1,000 出力トークン |
|:--|:--|:--|
| Claude 3.5 Sonnet | $0.003 | $0.015 |
| Claude 3.5 Haiku | $0.0008 | $0.004 |
| Claude 3 Opus | $0.015	 | $0.075 |
| Claude 3 Haiku | $0.00025 | $0.00125 |
| Claude 3 Sonnet | $0.003 | $0.015 |

OpenAI API

| モデル | 1,000 入力トークン | 1,000 出力トークン |
|:--|:--|:--|
| gpt-4o-mini | $0.00015 | $0.0006 |
| gpt-4o | $0.0025 | $0.01 |
| o1-mini | $0.003 | $0.012 |
| o1-preview | $0.015 | $0.06 |

| モデル | 画像解像度 | 標準品質 | プレミアム品質 |
|:--|:--|:--|:--|
| DALL·E 3 | 1024 x 1024 | $0.04 | $0.08 |
| DALL·E 3 | 1024×1792、1792×1024 | $0.08 | $0.12 |

Nova が他と比べて低コストなことは間違いなさそう

また、プロンプトキャッシュが利用された場合、さらに安く利用できます

https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html

## Livebook とは

Elixir のコードをブラウザで実行出来るツールです

https://livebook.dev/

例えば以下の画像のように、黒い枠（セル）の中にコードを書いて実行すると、そのすぐ下に結果が表示されます

![スクリーンショット 2023-07-26 9.59.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/61922a62-b29c-feac-a6f1-02058bcd359b.png)

公式サイトからインストーラーをダウンロードすれば、Windows でも macOS でもすぐに始められます

しかも、Webアプリの実装、デプロイから IoT まで何でもできてしまうので、これ一つあれば全てが賄えます

https://qiita.com/RyoWakabayashi/items/dc82ad3ed866d58a8acf

https://qiita.com/RyoWakabayashi/items/9cd0032d1bd4ddad5d6c

https://qiita.com/RyoWakabayashi/items/c203e40bd2004cb95aae

## 事前準備

以下の準備を済ませてください

- AWS のアカウント作成
- AWS の認証情報発行（アクセスキーIDとシークレットアクセスキー）
- us-east-1 リージョンでの Amazon Nova モデル利用申請
- Livebook のインストール

## セットアップ

Livebook で新しいトートブックを開き、セットアップセル（`Notebook dependencies and setup` と書いてある枠）に以下のコードを貼り付けてください

```elixir
Mix.install([
  {:aws, "~> 1.0"},
  {:hackney, "~> 1.20"},
  {:req, "~> 0.5"},
  {:kino, "~> 0.14"}
])
```

セルの左上 `Reconnect and setup` ボタンをクリックすると、処理に必要な依存モジュールがインストールされます

- [AWS Elixir](https://github.com/aws-beam/aws-elixir/)
- [Hackney](https://github.com/benoitc/hackney)
- [Req](https://github.com/wojtekmach/req)
- [Kino](https://github.com/livebook-dev/kino)

## AWS クライアントの準備

AWS 認証情報用の入力エリアを用意します

```elixir
access_key_id_input = Kino.Input.password("ACCESS_KEY_ID")
secret_access_key_input = Kino.Input.password("SECRET_ACCESS_KEY")
region_input = Kino.Input.text("REGION")

[
  access_key_id_input,
  secret_access_key_input,
  region_input
]
|> Kino.Layout.grid(columns: 3)
```

![スクリーンショット 2024-12-10 21.41.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e1dc40b9-729a-71cb-8aef-fa60bc3a2fd7.png)

表示された入力エリアにアクセスキーIDとシークレットアクセスキー、リージョンを入力してください

Nova を使うため、リージョンは `us-east-1` 固定です

入力した情報を使って AWS クライアントを作成します

```elixir
client =
  AWS.Client.create(
    Kino.Input.read(access_key_id_input),
    Kino.Input.read(secret_access_key_input),
    Kino.Input.read(region_input)
  )
```

## Nova モデル一覧取得

Bedrock の List foundation models API を呼び出し、名前が "Nova" で始まるモデルを抽出します

取得した結果は見やすく表形式にします

```elixir
models =
  client
  |> AWS.Bedrock.list_foundation_models()
  |> elem(1)
  |> Map.get("modelSummaries")
  |> Enum.filter(fn model ->
    String.starts_with?(model["modelName"], "Nova") and String.ends_with?(model["modelId"], ":0")
  end)

keys =
  [
    "providerName",
    "modelName",
    "modelId",
    "inputModalities",
    "outputModalities"
  ]

Kino.DataTable.new(models, keys: keys)
```

![スクリーンショット 2024-12-10 21.46.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6816bfc8-77ad-14fe-bfff-ee9e17f2acdf.png)

## Nova Micro

まずは最安モデル Nova Micro を使ってみましょう

上記の表を参照し、モデル ID を指定します

```elixir
model_id = "amazon.nova-micro-v1:0"
```

### テキスト理解

コード生成用のプロンプトを用意します

```elixir
code_generation_input = """
配列から指定数の要素を選択する組み合わせを全て生成するコードを Elixir で生成してください
コメントを日本語で適切につけてください
"""
```

以下のコードを実行し、 Bedrock 経由で Nova Micro を呼び出します

```elixir
results =
  client
  |> AWS.BedrockRuntime.invoke_model(
    model_id,
    %{
      "accept" => "application/json",
      "contentType" => "application/json",
      "messages" => [
        %{
          "role" => "user",
          "content" => [
            %{
              "text" => code_generation_input
            }
          ]
        }
      ]
    }
  )
  |> elem(1)
  |> Map.get("output")
```

実行結果

```elixir
%{
  "message" => %{
    "content" => [
      %{
        "text" => "Elixirで配列から指定数の要素を選択する..."
      }
    ],
    "role" => "assistant"
  }
}
```

このままだと読みにくいので、文章をマークダウンとして表示します

```elixir
results
|> Map.get("message")
|> Map.get("content")
|> hd()
|> Map.get("text")
|> Kino.Markdown.new()
```

実行結果

![スクリーンショット 2024-12-10 21.54.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6a0e3255-7de0-0ce7-08ea-58a96ca757d0.png)

ちなみにこのコードは `Enum.reverse() |> [result]` の部分でエラーが発生します

Elixir は Nova の情報源に不足しているようです

同じことを今後繰り返すので、テキスト理解の処理を関数化しておきます

```elixir
invoke_nova_text = fn input, model_id ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      model_id,
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "messages" => [
          %{
            "role" => "user",
            "content" => [%{"text" => input}]
          }
        ]
      },
      recv_timeout: 60_000
    )

  res
  |> Map.get("output")
  |> Map.get("message")
  |> Map.get("content")
  |> hd()
  |> Map.get("text")
end
```

日本のことをどれくらい理解しているか試してみます

```elixir
japanese_input = """
大分県について、以下の項目を教えてください
- 有名な戦国武将
- 有名な史跡
- 有名な郷土料理
- ご当地キャラクター
- 特徴的な方言
"""
```

```elixir
japanese_input
|> invoke_nova_text.("amazon.nova-micro-v1:0")
|> Kino.Markdown.new()
```

実行結果

```markdown
大分県について、以下の項目を詳しくご紹介します。

## 有名な戦国武将

大分県は戦国時代にも重要な役割を果たしました。特に有名な戦国武将としては、以下が挙げられます。

- 立花道雪：立花氏の当主で、戦国武将として有名です。立花山城（現大分県宇佐市）を拠点に、豊前国（現在の福岡県の一部）と豊後国（大分県）で活躍しました。立花道雪は弓の名手としても知られ、その武勇伝は今も語り継がれています。

## 有名な史跡

大分県には多くの歴史的な場所がありますが、特に有名な史跡は以下の通りです。

- 宇佐神宮：日本の八幡神を祀る神社で、国の重要文化財に指定されています。全国の八幡宮の総本宮とされており、古くから多くの参拝客を集めています。
- 別府温泉：世界的に有名な温泉地で、古くから湯治の場として栄えています。特に戦国時代には、大友氏や立花氏などの戦国大名たちが湯治に訪れました。

## 有名な郷土料理

大分県には独特の郷土料理が多くあります。

- からしれんこん：大分県を代表する郷土料理の一つで、辛味大根（からし）とレンコンを味噌ベースの濃厚なタレで煮込んだ料理です。辛味大根は別名「大分みそ」とも呼ばれます。
- 天重：大分県を代表する郷土料理の一つで、揚げた天ぷらを細かく切り、天重汁（みそベースの汁）に浸して食べます。具材は、レンコン、サツマイモ、タケノコ、ニンジン、キクラゲなど多様です。

## ご当地キャラクター

大分県には、以下のご当地キャラクターがあります。

- べっぷー：別府市のご当地キャラクターです。別府温泉のイメージキャラクターとして、温泉街の広報活動やイベントなどで活躍しています。
- おおいたちゃん：大分県の県民性や自然、文化を表現した公式キャラクターです。県庁のイメージキャラクターとして、県内外での広報活動に多用されています。

## 特徴的な方言

大分県は方言が豊富で、特に以下の特徴的な方言が挙げられます。

- 豊後方言（ほうごほうげん）：大分県の南部（旧豊後国）で話される方言で、標準語とは異なる独特のアクセントや表現があります。例えば、「食べる」を「たべる」ではなく「たびる」と発音することがあります。
- 豊前方言（ほうぜんほうげん）：大分県の北部（旧豊前国）で話される方言で、福岡県の北部方言と似ています。例えば、「うまい」を「めー」と発音することがあります。

これらの項目を通じて、大分県の文化や歴史を知ることができます。
```

立花道雪は実在するものの、かなりマニアックなところを突いてきました

史跡は正解しているものの、郷土料理以降はデタラメです

「めじろん」「べっぴょん」「ニータン」は流石に知らないか、、、

http://beppyon.gokuraku-jigoku-beppu.com/

https://www.pref.oita.jp/site/mejiron/

https://x.com/12neetan

## Nova Lite

中くらいの Nova Lite を使ってみましょう

### テキスト理解

まずは Micro と同じテキスト理解をやってみます

```elixir
code_generation_input
|> invoke_nova_text.("amazon.nova-lite-v1:0")
|> Kino.Markdown.new()
```

実行結果

![スクリーンショット 2024-12-10 22.06.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/215bd232-cd49-e981-ce40-9d817e662f92.png)

`Enum.combination` と言う関数が存在しないので、このコードもエラーになります

```elixir
japanese_input
|> invoke_nova_text.("amazon.nova-lite-v1:0")
|> Kino.Markdown.new()
```

実行結果

```markdown
もちろんです！大分県について以下項目を詳しく説明します。

## 有名な戦国武将

- 大友宗麟（おおとも そうりん）：大友氏の第19代当主で、大分県における戦国大名として有名です。宗麟はキリスト教（カトリック）を広め、キリシタン大名としても知られています。

## 有名な史跡

- 宇佐神宮：日本三大神宮の一つで、全国に約8,000社ある八幡宮の総本社です。
- 富貴寺：宗麟が建立した寺院で、国の重要文化財に指定されています。
- 耶馬渓：日本で最も美しい渓谷の一つとして知られ、国の特別名勝に指定されています。
- 別府地獄めぐり：温泉で有名な別府には、地獄と呼ばれる8つの温泉があり、それぞれ独特の景観と特徴を持っています。

## 有名な郷土料理

- ふぐ料理：大分県はふぐ料理で有名で、特に冬季にふぐを食べる習慣があります。
- もみじおろし：大分県が産地である馬肉をすり身にしてネギや唐辛子と和えた料理です。
- 豊後牛：大分県産の和牛で、その肉質の良さと柔らかさで知られています。
- 冷や汁：夏の季節限定の郷土料理で、豆腐や野菜を冷やして食べる料理です。

## ご当地キャラクター

- おおいたちゃん：大分県の公式キャラクターです。大分県の特産品や観光地を紹介する役割を担っています。

## 特徴的な方言

大分県の方言は大きく「北部方言」と「南部方言」に分かれます。

- 北部方言（中津・宇佐・豊後高田方言など）：アクセントは東京式アクセントに近く、語彙や発音が異なる部分があります。
- 南部方言（大分市・別府・佐伯方言など）：アクセントは京阪式アクセントに近く、語彙や発音が異なる部分があります。

例えば、「ありがとう」は北部では「おおきに」や「どうも」と言うことが多いです。南部では「ありがとう」が一般的ですが、方言のニュアンスや表現方法は地域によって異なります。
```

「もみじおろし」が大分とは聞いたことがないし、「冷や汁」は宮崎ですね

Micro よりは良くなりましたが、キャラクーと方言はデタラメです

### 画像理解

続いて画像理解用の関数を用意します

```elixir
invoke_nova_image = fn input_text, image, model_id ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      model_id,
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "messages" => [
          %{
            "role" => "user",
            "content" => [
              %{
                "image" => %{
                  "format" => "jpeg",
                  "source" => %{
                    "bytes" => Base.encode64(image)
                  }
                }
              },
              %{
                "text" => input_text
              }
            ]
          }
        ]
      },
      recv_timeout: 60_000
    )

  res
  |> Map.get("output")
  |> Map.get("message")
  |> Map.get("content")
  |> hd()
  |> Map.get("text")
end
```

入力としては以前 Qiita にアップロードした画像を使います

```elixir
image_url = "https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F1485835%2Fff6c4171-c83b-dc5b-afb3-fd94bc27498b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8cf5ad145f3859bb9901ab29b4f8efbf"
```

画像を WEB からダウンロードします

```elixir
image =
  image_url
  |> Req.get!()
  |> Map.get(:body)
```

実行結果

![スクリーンショット 2024-12-10 22.15.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/02110dfa-1cf0-8f99-2d9f-05aaaf1dcf72.png)

Nova Lite に説明してもらいましょう

```elixir
"画像に写っているものを詳細に教えてください"
|> invoke_nova_image.(image, "amazon.nova-lite-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
画像には、サンタクロースの工房をイメージした、クリスマスをテーマにしたイラストが描かれています。中央に、サンタクロースの手伝いをしている3人のエルフが描かれています。エルフたちは、それぞれが手に持っている用紙を読み、その用紙を箱に入れています。この箱には「PAGE SORTING」と書かれており、数字も表示されています。また、エルフたちは、サンタクロース用の手袋や靴を製造している工房にいるようにも見えます。背景には、クリスマスツリーやキャンディなどの装飾が施されています。また、画面には、エルフたちがいる部屋の天井に吊るされたクリスマスの飾り物がいくつか見られます。
```

かなり良く理解できているように思えます

「PAGE SORTING」の文字もちゃんと読めていますね

### 動画理解

続いて動画を理解してもらいます

動画は手元からアップロードします

```elixir
video_input = Kino.Input.file("VIDEO")
```

表示されたファイル選択エリアに MP4 ファイルをドラッグ&ドロップします

![スクリーンショット 2024-12-10 22.18.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8d5287ce-0e17-20a2-eb59-ad0008f9c860.png)

今回アップロードしたのは以下の動画です

<iframe width="1141" height="642" src="https://www.youtube.com/embed/gWx5W7viPO0" title="超音波センサーの距離計測結果をグラフ化" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

動画を読み込みます

```elixir
video =
  video_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> File.read!()
```

確認のため、動画を再生してみます

```elixir
Kino.Video.new(video, :mp4)
```

![スクリーンショット 2024-12-10 22.21.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/083dcb52-7986-227f-f0f4-17b471d4f6ca.png)

動画理解用の関数を用意します

```elixir
invoke_nova_video = fn input_text, video, model_id ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      model_id,
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "messages" => [
          %{
            "role" => "user",
            "content" => [
              %{
                "video" => %{
                  "format" => "mp4",
                  "source" => %{
                    "bytes" => Base.encode64(video)
                  }
                }
              },
              %{
                "text" => input_text
              }
            ]
          }
        ]
      },
      recv_timeout: 600_000
    )

  res
  |> Map.get("output")
  |> Map.get("message")
  |> Map.get("content")
  |> hd()
  |> Map.get("text")
end
```

Nova Lite に説明してもらいます

```elixir
"動画に写っているものを説明してください"
|> invoke_nova_video.(video, "amazon.nova-lite-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
The video shows a person working on a laptop with a colorful ribbon cable connected to a ribbon board. The laptop screen displays a graph, and the person is holding a box with Japanese text on it. The person is seen adjusting the ribbon cable, which has colored wires. The laptop is placed on a table, and there is a keyboard visible.
```

日本語で質問したのに英語で返ってきました

説明の内容としてはかなり正確に答えています

### 文書理解

続いて文書理解として、 PDF を読んでもらいます

中央区の ORIZURU WAVE というプロジェクトの PDF を与えてみましょう

```elixir
pdf_url = "https://www.city.chuo.lg.jp/documents/4167/chirashi.pdf"
```

https://www.city.chuo.lg.jp/documents/4167/chirashi.pdf

WEB から PDF をダウンロードします

```elixir
pdf =
  pdf_url
  |> Req.get!()
  |> Map.get(:body)
```

PDF 理解用の関数を用意し、実行します

```elixir
invoke_nova_doc = fn input_text, doc, model_id ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      model_id,
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "messages" => [
          %{
            "role" => "user",
            "content" => [
              %{
                "document" => %{
                  "format" => "pdf",
                  "name" => "計算書類",
                  "source" => %{
                    "bytes" => Base.encode64(doc)
                  }
                }
              },
              %{
                "text" => input_text
              }
            ]
          }
        ]
      },
      recv_timeout: 600_000
    )

  res
  |> Map.get("output")
  |> Map.get("message")
  |> Map.get("content")
  |> hd()
  |> Map.get("text")
end
```

```elixir
"何をきっかけとした取り組みですか"
|> invoke_nova_doc.(pdf, "amazon.nova-lite-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
東京2020オリンピック・パラリンピック
```

端的な回答ですが、正解しています

## Nova Pro

最高位モデルの Nova Pro を使ってみます

### テキスト理解

```elixir
code_generation_input
|> invoke_nova_text.("amazon.nova-pro-v1:0")
|> Kino.Markdown.new()
```

実行結果

![スクリーンショット 2024-12-10 22.29.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/32184e64-810b-a1b1-360e-39daa8180bee.png)

残念ながら、　Lite と同じく `Enum.combination` が存在しないため動きません

```elixir
japanese_input
|> invoke_nova_text.("amazon.nova-pro-v1:0")
|> Kino.Markdown.new()
```

```markdown
大分県に関する情報を以下にまとめます。

## 有名な戦国武将

- 大友宗麟：大友氏の当主で、戦国時代に九州で大きな勢力を持っていました。キリスト教に傾倒し、キリシタン大名としても知られています。

## 有名な史跡

- 宇佐神宮：全国の八幡宮の総本宮とされる由緒正しい神社です。
- 耶馬渓：大分と福岡にまたがる美しい渓谷で、多くの文人墨客に愛された風光明媚な場所です。
- 別府温泉：日本三大秘湯のひとつで、多くの温泉地が点在しています。

## 有名な郷土料理

- とり天：鶏肉の衣揚げで、大分県を代表するB級グルメです。
- だんご汁：麦味噌ベースの汁に Various ingredients such as dumplings, vegetables, and sometimes meat are added.

## ご当地キャラクター

- おおいたさん：大分県の魅力をPRする公式キャラクターで、温泉と湯布院の自然をイメージしたデザインです。

## 特徴的な方言

- 〜とぅ：「〜ところ」の意味で使われる。「こことぅ（ここところ）」など。
- ばってん：「しかし」の意味。九州地方全体でよく使われますが、大分でも使用されます。

以上が大分県に関する主な情報です。
```

かなりいい感じにはなりましたが、「だんご汁」の説明が途中から英語になっているのと、方言がやはり間違ってますね

「おおいたさん」は初めて聞きました

https://dic.pixiv.net/a/%E5%A4%A7%E5%88%86%E3%81%95%E3%82%93%2847ts%29

### 画像理解

```elixir
"画像に写っているものを詳細に教えてください"
|> invoke_nova_image.(image, "amazon.nova-pro-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
画像には、3人の小さなエルフが描かれている。彼らは、サンタクロースの帽子を被り、緑と赤のストライプの衣装を着ている。エルフたちは、クリスマスツリーや星などのクリスマスの装飾品が飾られた部屋で、机に向かって座っている。机の上には、数冊の本が積み重ねられており、エルフたちはその本を読んでいる。また、彼らは、小さな木製の箱の中に、さらに多くの本を整理している。部屋は、暖かい雰囲気で、窓からは、星や雪が降り注いでいる様子が描かれている。
```

おおよその理解は合っているものの、「本」ではないですね

### 動画理解

```elixir
"動画に写っているものを説明してください"
|> invoke_nova_video.(video, "amazon.nova-pro-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
動画には、ラップトップコンピューター、赤、黄、緑、青の4本のワイヤー、それらのワイヤーが接続されている小さな電子機器、そして箱が写っています。
```

動作は説明していないものの、日本語で返してくれました

### 文書理解

```elixir
"何をきっかけとした取り組みですか"
|> invoke_nova_doc.(pdf, "amazon.nova-pro-v1:0")
|> Kino.Markdown.new()
```

実行結果

```text
東京2020オリンピック・パラリンピック競技大会をきっかけとして、国際平和と文化交流を目的とした取り組みです。
```

しっかり文章で返してくれました

## Nova Canvas

画像生成や画像加工ができる Nova Canvas を使ってみます

### 画像生成

画像生成用の関数を用意します

```elixir
generate_image = fn input_text ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      "amazon.nova-canvas-v1:0",
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "taskType" => "TEXT_IMAGE",
        "textToImageParams" => %{
          "text" => input_text
        }
      },
      recv_timeout: 600_000
    )

  res
  |> Map.get("images")
  |> hd()
  |> Base.decode64!()
end
```

あえて、 AI が苦手な指示を出してみます

```elixir
generate_image.("ラーメンを食べる人")
```

実行結果

![スクリーンショット 2024-12-10 22.48.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bdbe07a3-84b2-32e8-d5bb-e42b056fa072.png)

禁断の両手箸!

やはり日本文化にはまだまだ疎いですね

### 背景削除

画像から背景を削除してみます

```elixir
remove_image_background = fn image ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      "amazon.nova-canvas-v1:0",
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "taskType" => "BACKGROUND_REMOVAL",
        "backgroundRemovalParams" => %{
          "image" => Base.encode64(image)
        }
      },
      recv_timeout: 600_000
    )

  res
  |> Map.get("images")
  |> hd()
  |> Base.decode64!()
end
```

```elixir
remove_image_background.(image)
```

実行結果

![スクリーンショット 2024-12-10 22.50.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d37af49a-0a53-089b-1a8f-538416b290d3.png)

いいところまではいっていますが、やはりイラストだと難しいようですね

### 画像編集

テキストと画像の両方を与えて、画像の編集指示を出すこともできます

```elixir
edit_image = fn input_text, image ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.invoke_model(
      "amazon.nova-canvas-v1:0",
      %{
        "accept" => "application/json",
        "contentType" => "application/json",
        "taskType" => "TEXT_IMAGE",
        "textToImageParams" => %{
          "conditionImage" => Base.encode64(image),
          "text" => input_text
        }
      },
      recv_timeout: 600_000
    )

  res
  |> Map.get("images")
  |> hd()
  |> Base.decode64!()
end
```

```elixir
edit_image.("和風にして", image)
```

実行結果

![スクリーンショット 2024-12-10 23.01.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cca9d391-be46-2cda-e4b5-4d615ccfbb76.png)

着物だったり桜だったり、確かに和風になりました

## Nova Reel

Nova Reel で動画を生成してみましょう

動画生成には数分間かかるため、呼び出しを非同期にします

```elixir
generate_video = fn input_text, s3_uri ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.start_async_invoke(%{
      "modelId" => "amazon.nova-reel-v1:0",
      "modelInput" => %{
        "taskType" => "TEXT_VIDEO",
        "textToVideoParams" => %{
          "text" => input_text
        },
        "videoGenerationConfig" => %{
          "durationSeconds" => 6,
          "fps" => 24,
          "dimension" => "1280x720", 
          "seed" => 12
        }
      },
      "outputDataConfig" => %{
        "s3OutputDataConfig" => %{
          "s3Uri" => s3_uri
        }
      }
    })

  res
end
```

テキスト入力で出力先を指定します

```elixir
s3_uri_input = Kino.Input.text("S3_URI")
```

```elixir
s3_uri = Kino.Input.read(s3_uri_input)
```

```elixir
generate_video.("クリスマスにサンタがダンスする可愛いアニメーション", s3_uri)
```

以下のコードを何回か実行し、 Status が "Completed" になるまで待ちます

```elixir
invokes =
  client
  |> AWS.BedrockRuntime.list_async_invokes()
  |> elem(1)
  |> Map.get("asyncInvokeSummaries")
```

動画生成中の実行結果

```elixir
[
  %{
    "clientRequestToken" => "xxx",
    "invocationArn" => "arn:aws:bedrock:us-east-1:999:async-invoke/xxx",
    "lastModifiedTime" => "2024-12-10T10:15:20Z",
    "modelArn" => "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-reel-v1:0",
    "outputDataConfig" => %{
      "s3OutputDataConfig" => %{"s3Uri" => "s3://xxx/bedrock/xxx"}
    },
    "status" => "InProgress",
    "submitTime" => "2024-12-10T10:15:03Z"
  }
]
```

動画生成完了後の実行結果

```elixir
[
  %{
    "clientRequestToken" => "xxx",
    "endTime" => "2024-12-10T10:19:21Z",
    "invocationArn" => "arn:aws:bedrock:us-east-1:999:async-invoke/xxx",
    "lastModifiedTime" => "2024-12-10T10:19:22Z",
    "modelArn" => "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-reel-v1:0",
    "outputDataConfig" => %{
      "s3OutputDataConfig" => %{"s3Uri" => "s3://xxx/bedrock/xxx"}
    },
    "status" => "Completed",
    "submitTime" => "2024-12-10T10:15:03Z"
  }
]
```

実行結果から出力先のファイル名を取得します

```elixir
video_url =
  invokes
  |> hd()
  |> Map.get("outputDataConfig")
  |> Map.get("s3OutputDataConfig")
  |> Map.get("s3Uri")
  |> Kernel.<>("/output.mp4")
```

URL からバケット名、オブジェクトキーを取得します

```elixir
bucket = video_url |> String.split("/") |> Enum.at(2)
key = video_url |> String.split("/") |> Enum.slice(3..-1//1) |> Enum.join("/")
```

動画を S3 からダウンロードします

```elixir
{:ok, %{"Body" => generated_video}, _} = AWS.S3.get_object(client, bucket, key)
```

動画を Livebook に表示してみます

```elixir
Kino.Video.new(generated_video, :mp4)
```

![nova.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ddd1b9cf-99a1-4afa-fd27-64aa3e386afa.gif)

### 静止画の動画化

画像とテキストを与えて動画を生成します

```elixir
animate_image = fn input_text, image, s3_uri ->
  {:ok, res, _} =
    client
    |> AWS.BedrockRuntime.start_async_invoke(%{
      "modelId" => "amazon.nova-reel-v1:0",
      "modelInput" => %{
        "taskType" => "TEXT_VIDEO",
        "textToVideoParams" => %{
          "text" => input_text,
          "images" => [
            %{
              "format" => "jpeg",
              "source" => %{
                "bytes" => Base.encode64(image)
              }
            }
          ]
        },
        "videoGenerationConfig" => %{
          "durationSeconds" => 6,
          "fps" => 24,
          "dimension" => "1280x720", 
          "seed" => 12
        }
      },
      "outputDataConfig" => %{
        "s3OutputDataConfig" => %{
          "s3Uri" => s3_uri
        }
      }
    })

  res
end
```

動画の元にする画像は以下の条件を満たしている必要があります

- 1280x720 のサイズであること
- アルファチャネルがないこと

該当するようなファイルを用意してアップロードしましょう

```elixir
start_image_input = Kino.Input.image("IMAGE", format: :jpeg)
```

```elixir
start_image =
  start_image_input
  |> Kino.Input.read()
  |> Map.get(:file_ref)
  |> Kino.Input.file_path()
  |> File.read!()
```

```elixir
animate_image.("ダンスさせてください", start_image, s3_uri)
```

完成するまで待ちます

```elixir
invokes =
  client
  |> AWS.BedrockRuntime.list_async_invokes()
  |> elem(1)
  |> Map.get("asyncInvokeSummaries")
```

実行結果の動画を元の静止画と並べて確認します

```elixir
video_url =
  invokes
  |> hd()
  |> Map.get("outputDataConfig")
  |> Map.get("s3OutputDataConfig")
  |> Map.get("s3Uri")
  |> Kernel.<>("/output.mp4")

bucket = video_url |> String.split("/") |> Enum.at(2)
key = video_url |> String.split("/") |> Enum.slice(3..-1//1) |> Enum.join("/")

{:ok, %{"Body" => generated_video}, _} = AWS.S3.get_object(client, bucket, key)

Kino.Layout.grid([
  start_image,
  Kino.Video.new(generated_video, :mp4)
])
```

![nova_dance.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6cd0ac3a-bea3-4c6a-3fa5-265f25bd00c2.gif)

顔だけの画像にダンスさせたのが悪いかもしれませんが、中々怖い動画になりました

## おまけ

### ChatGPT に同じ質問をしてみた

ChatGPT の 4o に同じテキストの質問をしてみました

:::note info
ChatGPT の場合、内部的に回答の検証等もしているのでフェアではないです

あくまでも参考程度にしてください
:::

コード生成の回答は完璧です（ちゃんと動きました）

![スクリーンショット 2024-12-11 8.46.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/58e48e4f-110a-da4c-bb85-4f424d55535e.png)

「めじろん」や「りゅうきゅう」も知っているし、方言の回答も完璧でした

インターネット検索はオフにしていたはずではあるんですが、ネットから情報を拾っているっぽい

やはりエージェントを使って情報収集するのが最強ということですかね

![スクリーンショット 2024-12-11 8.51.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/14c9bbec-17a5-35a5-38fe-0743df67cdc0.png)

「ラーメンを食べる人」は、まあまあですかね

片手がだらんと下がっているのが気になります

![gpt_ramen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4c255221-bd79-b0d6-08cf-a9457ad4a85e.png)

### Vidu で動画生成してみた

Vidu というサービスに静止画から動画を生成してもらいました

https://www.vidu.studio/

![vidu.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d89759b4-f102-f96d-cd98-bdab860b6fe0.gif)

カッコよすぎ、、、

## まとめ

Livebook から Amazon Nova のモデルを呼び出し、動画生成など色々なことができました

画像生成と動画生成に関しては、一昔前のクオリティというのが正直な感想ですね、、、

ただ、価格は確かに安いので、用途によっては使えそうです

ファインチューニングして蒸留すれば、もっと実用的なモデルにできそうですね