Gemini-1.5-flashに音声と動画を理解させる（かんたん＆すごい）

# Gemini-1.5-flash
Gemini 1.5の新バージョンであるgemini-1.5-flashの性能を試してみたいと思います！
gemini1.5-flashはコンテキストウィンドウが1Mを誇り、動画や音声を適当に投げても、相当な精度で内容を理解してくれます。

今回のアップデートでどれほどの能力を身につけたのか、早速試してみます。

https://blog.google/intl/ja-jp/company-news/technology/google-gemini-update-flash-io-2024/
# 音声データの認識
gemini-1.5-flashでは、音声データをそのままプロンプトにくっつけて投げることができます。
ただし、鵜呑みにしてファイルをプロンプトにくっつけて投げると、8MBまでの容量制限を受けることになります。
そのため、GCPのCloud Strageにファイルをアップロードして、そのURIをモデルの入力とします。

さくっとStreamlitでアプリを作ってみました。
```python:Gemini_Gijiroku.py
import streamlit as st
from google.cloud import storage
from google.generativeai import GenerativeModel
import base64
import vertexai
from vertexai.generative_models import GenerativeModel, Part, FinishReason
import vertexai.preview.generative_models as generative_models
import time

# GCPの設定 (Cloud Storage, Gemini API)
BUCKET_NAME = 'CloudStrageのバケット名'
SERVICE_ACCOUNT_KEY_FILE = 'サービスアカウントのキーファイルへのパス'
PROJECT_NAME = 'プロジェクト名'
REGION_NAME = 'リージョン'

storage_client = storage.Client.from_service_account_json(SERVICE_ACCOUNT_KEY_FILE)
bucket = storage_client.bucket(BUCKET_NAME)

def generate(audio_file):
  vertexai.init(project=PROJECT_NAME, location=REGION_NAME)
  model = GenerativeModel(
    "gemini-1.5-flash-preview-0514",
  )
  responses = model.generate_content(
      [audio_file, """この音声は、ある会議の音声です。この音声をもとに会議の要約を作成してください。
音声に含まれていない情報をでっち上げたり、冗長になったりしてはいけません。

まず、会議の概要、何が議論されたのか、会議の結論、アクションアイテムを冒頭にまとめて記載してください。
会議の概要には、会議名、日時、参加者が必要です。開催日時などが不明の場合は「不明」と記載してください。
また内容はMECEである必要があります。

その後、会議の流れを記載します。
会議の流れは下記の形式で、それぞれのタイムスタンプごとの話題の概要を記載します。
もしも補足するべき内容があれば、備考に記載してください。表の中はすべて左詰となるようにします。
|タイムスタンプ|話題|備考|
"""],
      generation_config=generation_config,
      safety_settings=safety_settings,
      stream=True,
  )

  return responses

def delete_all_blobs(bucket_name):
    bucket = storage_client.bucket(bucket_name)
    blobs = list(bucket.list_blobs())
    for blob in blobs:
        blob.delete()
    
    st.toast("全てのBlobを削除しました", icon='😃')

safety_settings = {
    generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
}

st.set_page_config(
    page_title="Gemini-1.5-pro / マルチモーダル",
    page_icon="🚀",
    layout="wide",
    initial_sidebar_state="expanded",
)

with st.sidebar:
    st.subheader('Generation config')
    max_output_tokens = st.slider(label="出力トークンサイズ", min_value=1024, max_value=8192, value=8192)
    temperature = st.slider(label="Temperature", min_value=0.0, max_value=1.0, value=1.0)
    top_p = st.slider(label="Top P", min_value=0.0, max_value=1.0, value=0.95)
    st.subheader('Cloud Storage')
    st.button(label="Cloud Strage内のファイルを全て消去", on_click=delete_all_blobs, args=(BUCKET_NAME,))
    
generation_config = {
    "max_output_tokens": max_output_tokens,
    "temperature": temperature,
    "top_p": top_p,
}

st.title('音声ファイルから議事録を作る')

uploaded_file = st.file_uploader("音声ファイルを選択します", type=["mp3", "wav"])

if uploaded_file is not None:
    with st.status("議事録を作成しています...", expanded=True) as status:
        # ステータス表示
        status_text = st.empty()  # ステータスメッセージを表示する場所を確保

        # Cloud Storageにアップロード
        blob = bucket.blob(uploaded_file.name)
        status.write("音声ファイルをアップロードしています...")  # アップロード中であることを表示
        blob.upload_from_file(uploaded_file)
        status.write("議事録を作成しています...")  # アップロード中であることを表示

        # Gemini APIで議事録作成
        gcs_uri = f'gs://{BUCKET_NAME}/{uploaded_file.name}'
        audio1 = Part.from_uri(
            mime_type="audio/wav",
            uri=gcs_uri)

        response = generate(audio1)
        status.update(label="完了しました", expanded=False, state="complete") 

    # 議事録をリアルタイム表示
    minutes_container = st.empty()
    minutes = ""
    for chunk in response: # type: ignore
        minutes += chunk.text
        minutes_container.write(minutes)
        time.sleep(0.05)
```
### 動かしてみる

サクッと機能を試したいだけなので、いろいろ汚いコードになってしまいました…
Streamlitで簡単に動かしてみます。

```shell
streamlit run Gemini_Gijiroku.py
```
![スクリーンショット 2024-05-16 16.44.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/ba48dd3d-4a4a-3160-ed88-273e3f9a0cf4.png)
画面が立ち上がりましたので、早速音声ファイルを入力してみます。
今回は下記のnoteで公開されていた、新入社員向けの議事録練習用の会議音声を利用します（そんなのあるんですね…）

https://note.com/note_0530/n/neb6a205744bd

### 結果
![スクリーンショット 2024-05-16 16.46.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/9d91e6b8-a69c-9e56-9c37-4b81ee5a9f0c.png)

会議の内容を正確に捉えています。
会議の流れもまとめるように指示していたので、その結果も見てみましょう。

![スクリーンショット 2024-05-16 16.47.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/1d03e4a9-2ff7-0b45-18c3-b9c5e0a78621.png)

完璧ですね。後から会議の模様をキャッチアップすることも簡単です。
ただ音声ファイルをそのまま投げただけでこの精度とは、恐れ入ります。

## 動画データの認識
同様の手順で動画データもそのままモデルへの入力とすることができます。
Gemini-1.5では、動画中の音声データもそのまま理解してくれるとのことなので、面倒な事前処理は不要です。
先ほどのコードを少し編集し、これも試してみます。

```python:Gemini_Douga.py
import streamlit as st
from google.cloud import storage
from google.generativeai import GenerativeModel
import base64
import vertexai
from vertexai.generative_models import GenerativeModel, Part, FinishReason
import vertexai.preview.generative_models as generative_models
import time

# GCPの設定 (Cloud Storage, Gemini API)
BUCKET_NAME = 'CloudStrageのバケット名'
SERVICE_ACCOUNT_KEY_FILE = 'サービスアカウントのキーファイルへのパス'
PROJECT_NAME = 'プロジェクト名'
REGION_NAME = 'リージョン'

storage_client = storage.Client.from_service_account_json(SERVICE_ACCOUNT_KEY_FILE)
bucket = storage_client.bucket(BUCKET_NAME)

def generate(audio_file):
  vertexai.init(project=PROJECT_NAME, location=REGION_NAME)
  model = GenerativeModel(
    "gemini-1.5-flash-preview-0514",
  )

  contents = [video_file, """この動画は、何かのコンテキストを持っています。この動画をもとに要約を作成してください。
動画に含まれていない情報をでっち上げたり、冗長になったりしてはいけません。

まず、動画の概要、被写体は何か、コンテキストは何かを記載します。
コンテキストが不明な場合は、推測して記載してください。その際は「（推測）」と末尾につけてください。
フォーマットは下記の通りとします：
## 動画の概要
<動画の概要を記載>
### コンテキスト
<コンテキストを記載>

その後、動画の流れを記載します。
動画の流れは下記の形式で、それぞれのタイムスタンプごとの概要を記載します。
もしも補足するべき内容があれば、備考に記載してください。表の中はすべて左詰となるようにします。
### 動画の流れ
|タイムスタンプ|コンテキスト|備考|
"""]
  
  tokens = model.count_tokens(contents)

  responses = model.generate_content(
      contents,
      generation_config=generation_config,
      safety_settings=safety_settings,
      stream=True,
  )

  return [responses, tokens]

def delete_all_blobs(bucket_name):
    bucket = storage_client.bucket(bucket_name)
    blobs = list(bucket.list_blobs())
    for blob in blobs:
        blob.delete()
    
    st.toast("全てのBlobを削除しました", icon='😃')

safety_settings = {
    generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
    generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
}

st.set_page_config(
    page_title="Gemini-1.5-pro / マルチモーダル",
    page_icon="🚀",
    layout="wide",
    initial_sidebar_state="expanded",
)

with st.sidebar:
    st.subheader('Generation config')
    max_output_tokens = st.slider(label="出力トークンサイズ", min_value=1024, max_value=8192, value=8192)
    temperature = st.slider(label="Temperature", min_value=0.0, max_value=1.0, value=1.0)
    top_p = st.slider(label="Top P", min_value=0.0, max_value=1.0, value=0.95)
    st.subheader('Cloud Storage')
    st.button(label="Cloud Strage内のファイルを全て消去", on_click=delete_all_blobs, args=(BUCKET_NAME,))
    
generation_config = {
    "max_output_tokens": max_output_tokens,
    "temperature": temperature,
    "top_p": top_p,
}

st.title('動画ファイルを読み込んで内容を解釈する')

uploaded_file = st.file_uploader("動画ファイルを選択します", type=["mp4"])

if uploaded_file is not None:
    with st.status("動画を分析しています...", expanded=True) as status:
        # ステータス表示
        status_text = st.empty()  # ステータスメッセージを表示する場所を確保

        # Cloud Storageにアップロード
        blob = bucket.blob(uploaded_file.name)
        status.write("動画ファイルをアップロードしています...")  # アップロード中であることを表示
        blob.upload_from_file(uploaded_file)
        status.write("回答を準備しています...")  # アップロード中であることを表示

        # Gemini APIで議事録作成
        gcs_uri = f'gs://{BUCKET_NAME}/{uploaded_file.name}'
        video = Part.from_uri(
            mime_type="video/mp4",
            uri=gcs_uri)

        
        response_datas = generate(video)
        response = response_datas[0]
        tokens = response_datas[1]
        status.update(label=f"完了しました。トークン数：{tokens.total_tokens}", expanded=False, state="complete") 

    # 議事録をリアルタイム表示
    minutes_container = st.empty()
    minutes = ""
    for chunk in response: # type: ignore
        minutes += chunk.text
        minutes_container.write(minutes)
        time.sleep(0.05)
```

ほとんど何も変わっていませんね。
動画データの解釈のために、プロンプトだけ変更しています。

これもStreamlitで起動してみます。

```terminal
streamlit run Gemini_Douga.py
```

![スクリーンショット 2024-05-16 17.04.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/c971803f-329f-385b-c0a4-1dfefe94040a.png)

### 結果
今回は総務省が公開している、BS・CSの4K・8K放送についての紹介動画を読み込ませてみました。
こういう動画は見る気は起きませんが、みなくても内容がわかれば嬉しいですよね。

https://www.soumu.go.jp/menu_seisaku/ictseisaku/housou_suishin/4k8k_suishin_download.html

今回使ったのは``1．4K・8Kの魅力``という動画ファイルで、およそ4分の長さがあります。
早速結果を見てみましょう。

![スクリーンショット 2024-05-16 17.08.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/9bf559f1-f943-c578-a6fa-a0f6c69e5112.png)

動画の概要やコンテキストをしっかりと認識してくれています。
また音声データと同様に、動画の中身をタイムスタンプ付きで説明してもらいましたが、こちらも非常に正確でした。

# まとめ
Gemini-1.5のマルチモーダル性能は強力でした。
特にファイルをそのままモデルに投げるだけで、中身を理解して返答してくれますので、これは活用の幅が広がりそうです。

コンテキストウィンドウも今後2Mへ拡大されるとのことで、よりマルチモーダルの能力を生かした使い方ができそうですね。

## おまけ（GPT4o）
![スクリーンショット 2024-05-16 17.13.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791831/fbf5ca23-2734-8d96-275f-efa3818cadac.png)

ちがうちがうそうじゃない
ちょっと横着してGUIから投げたからかもしれません