OpenAIとStreamlitで画像を英語で描写する練習ができるアプリを作ってみた

OpenAIとStreamlitで画像を英語で描写する練習ができるアプリを作ってみた

Clock Icon2024.07.11

はじめに

AIは日常の様々な場面で役に立ちますが、私は特に語学学習において便利であると感じています。過去にも英語日記をAIに添削してもらいWordPressに投稿するという記事を投稿しました。普段においても、ChatGPTを使って雑に英会話の練習をしたり、わからない単語に出会ったときには意味はもちろん、用例や類似する単語とのニュアンスの違いも解説してくれます。

今回は、AIに画像を生成してもらい、画像についての英語での描写をAIに添削してもらうようなアプリを作成してみました。

成果物

最終的に出来上がるのは以下のようなアプリです。

初期状態です。

20240711_en_01

「画像を生成」ボタンをクリックすると、お題となる画像と、入力欄が表示されます。入力欄には画像を英語で描写したものを入力します。

20240711_en_02

「回答をチェック」を押すと、AIの添削結果が表示されます。

20240711_en_03

前提

  • Windows10
  • Python 3.12.1
  • OpenAIのAPIキーの取得や環境変数の設定などは終わっているものとします
  • ライブラリのインストール手順は省略します

画像の生成

画像の生成はdall-e-2を使って行いました。執筆時点でdall-e-3が最新ですが、私のプロンプトの問題なのか、dall-e-2で生成した画像の方が私の希望に近かったためです。

OpenAI Platform

画像の生成を行う関数のコードは以下のようになります。生成した画像のURLが返ってきますが、描写するためには目で確認する必要があるため、ファイルに保存するようにしています。

get_problem関数の引数はどんな画像を生成するかを指定できるようにしたもので、例えば「学生生活」、「ビジネス」、「自然」などのキーワードをイメージしています。

from openai import OpenAI
import requests

openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)

def get_problem(theme):
    generated = client.images.generate(
        model="dall-e-2",
        prompt=f"{theme}の一場面を写した写真を生成してください。",
        size="512x512",
        quality="standard",
        n=1,
    )

    data = requests.get(generated.data[0].url)
    with open("image.jpg", "wb") as f:
        f.write(data.content)
    return generated.data[0].url

AIによる添削

OpenAI Platform

保存された画像を見て、ユーザはその画像を英語で描写します。正しく描写できているか、文章に誤りがないかなどをAIにチェックしてもらう関数は以下のようになります。

check_answer関数の引数はそれぞれ生成された画像のURLと、ユーザが描写した英文です。

image_urluserロールでしか使えないので注意してください。

def check_answer(image_url, answer):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_url,
                        },
                    },
                ],
            }
        ],
    )

    return response.choices[0].message.content

一般的にプロンプトは英語の方が少ないトークン数で済みます。今回はわかりやすさのために日本語にしていますが、コストが気になる場合は英語に翻訳した方が良いかもしれません。

実行してみる

ここまで画像の生成、AIによる添削の二つの関数を作成したので、実際に呼び出してみます。二つの関数の定義より下に、以下のコードを記述します。

image_url = get_problem("学生生活")
my_answer = input(
    "フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)
ai_comment = check_answer(image_url=image_url, answer=my_answer)
print(ai_comment)

ファイルを実行します。

python main.py

実行後しばらくすると、コードファイルと同じ階層にimage.jpgが作成されます。

20240711_en_04

コンソールに指示が表示されるので、指示通り英語で描写したものを入力します。

20240711_en_05

フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: A girl is studying. She looks like struggling with her studies.

Enterを押すと、しばらくしてAIによる添削結果が表示されます。

20240711_en_06

あなたの描写「A girl is studying. She looks like struggling with her studies.」はおおむね正しいですが、少し改善できます。以下に詳しく説明します。

1. "like" の使い方: 英文では、「looks like」の後には通常名詞か代名詞を使用します。動詞が続く場合は、「looks as if」や「looks like she is」を使うのが一般的です。
2. 冗長性: 2つの文の間により良い接続を考慮すると、文が滑らかになります。

したがって、より適切な英訳は以下のようになります:
"A girl is studying. She looks as if she is struggling with her studies."

日本語で説明すると、以下のようなポイントに気をつけて改善された英文を使います:
1. 最初の「like」の使い方を修正し、「She looks as if she is」を使う。
2. 文をよりスムーズに繋げるために、完全な表現に変更する。

その結果、次のような改善された英文が得られます:
"A girl is studying. She looks as if she is struggling with her studies."

いい感じですね。

試しに誤った描写をしてみます。

20240711_en_07

フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: There are many boys. They are playing baseball.
この画像を基にした描写「There are many boys. They are playing baseball.」は正しくありません。

画像には、1人の女の子が写っています。彼女は制服を着ていて、視線を横に向けているようです。「There are many boys. They are playing baseball.」の文は、この画像の状況を正確に反映して
いません。

より適切な英文の描写例としては:
「There is a girl wearing a uniform. She is looking to the side.」
が考えられます。

日本語での解説:
- 「There are many boys.」は「たくさんの男の子がいます」という意味ですが、この画像には男の子たちは描かれていません。
- 「They are playing baseball.」は「彼らは野球をしています」という意味ですが、画像の女の子は野球をしていません。
- よって、正確な描写は「制服を着た女の子がいます。彼女は横を向いています。」といった説明が適しています。

こちらもいい感じですね。

ここまでのコードは以下の通りです。

from openai import OpenAI
import os
import requests

openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)

def get_problem(theme):
    generated = client.images.generate(
        model="dall-e-2",
        prompt=f"{theme}の一場面を写した写真を生成してください。",
        size="512x512",
        quality="standard",
        n=1,
    )

    data = requests.get(generated.data[0].url)
    with open("image.jpg", "wb") as f:
        f.write(data.content)
    return generated.data[0].url

def check_answer(image_url, answer):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_url,
                        },
                    },
                ],
            }
        ],
    )

    return response.choices[0].message.content

image_url = get_problem("学生生活")
my_answer = input(
    "フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)
ai_comment = check_answer(image_url=image_url, answer=my_answer)
print(ai_comment)

これで完成…と言いたいところですが、コンソールでいちいち実行するのは少し手間ですよね。できればUIが欲しいところです。

というわけで続いてこのアプリにUIをつけていきます。

UIの作成

といっても、UIを作るのは大変そうですよね。Webアプリにするのだったら、HTMLやCSSも書かないといけない、と思われるかもしれません。しかも、せっかくここまでPythonコードを書いてアプリが出来上がったのに、また大幅な変更を加えないといけないかもしれない、とも思われるかもしれません。

そこで今回はStreamlitというPythonのライブラリを使います。

Streamlit • A faster way to build and share data apps

このライブラリを簡単に説明すると、

  • PythonコードだけでフロントエンドのUIが作れる
  • HTML、CSSは一切不要
  • GitHubリポジトリから公式のクラウドサービスに無料で爆速デプロイできる

というものです。

百聞は一見にしかずということで、まずはライブラリをインストールします。

pip install streamlit

Streamlitのコマンドは以下の通りです。

streamlit run main.py

とりあえず現在のファイルのまま上記コマンドを実行してみます。ローカルのWebサーバーが立ち上がり、自動的にブラウザで表示されます。今は何もしていないので、何も表示されません。

20240711_en_08

※ちなみに処理はちゃんと動いているので、コンソールに「フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。:」という文章が表示され、image.jpgも生成されています。

Ctrl + Cで実行を止めます。

ではまず、このWebアプリにタイトルをつけてみます。二つの関数の定義が終わり、get_problem関数を呼び出す手前に1行コードを追加します。

st.title("画像を英文で描写する問題ジェネレーター")  # これを追加

image_url = get_problem("学生生活")
my_answer = input(
    "フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "

実行すると、タイトルがつきました。

20240711_en_09

続いて、生成する画像のテーマを選べるようにしてみます。タイトルの下にコードを追加します。

st.title("画像を英文で描写する問題ジェネレーター")

theme = st.selectbox(
    "テーマを選択してください",
    ["学生生活", "旅行", "仕事", "スポーツ", "自然"],
)

実行すると、テーマをセレクトボックスで選べるようになりました。

20240711_en_10

もうおわかりでしょうか?このように、少しのPythonコードを書くだけで、HTMLやCSSを一切書かなくても上記のようなリッチなUIを自動で作成してくれるのがStreamlitです。

さて、続いてボタンを押したらAIに画像を生成させるようにしてみます。

if st.button("画像を生成"):
    with st.spinner("画像を生成中..."):
        image_url = get_problem(theme)
        st.image(image_url)

実行すると、ボタンが表示されます。

20240711_en_11

ボタンを押すと、ローディング中のメッセージが表示されます。

20240711_en_12

AIが画像を生成し終えると、画像が表示されます。

20240711_en_13

続いてユーザがこの画像を元に英文を入力できるように、テキストエリアを作成します。

if st.button("画像を生成"):
    with st.spinner("画像を生成中..."):
        image_url = get_problem(theme)
        st.image(image_url)

        st.write("生成された画像を見て、画像を英文で描写してください。")
        my_answer = st.text_area("英文の回答", height=200)

画像の生成が完了すると、画像の下に入力欄が表示されます。

20240711_en_14

以下のコードはもう不要なので削除します。

my_answer = input(
    "フォルダに生成されたimage.jpgを見て、画像を英文で描写してください。: "
)

また、get_problem関数内でimage.jpgを保存している箇所も不要なので削除します。

この画像と入力内容を元にAIに添削してもらうよう、もう一つボタンを作成します。まずは、先ほど作成したボタンと同じようにコードを書いてみます。

if st.button("画像を生成"):
    with st.spinner("画像を生成中..."):
        image_url = get_problem(theme)
        st.image(image_url)

        st.write("生成された画像を見て、画像を英文で描写してください。")
        my_answer = st.text_area("英文の回答", height=200)

        if st.button("回答をチェック"):
            with st.spinner("チェック中..."):
                ai_comment = check_answer(image_url=image_url, answer=my_answer)
                st.write(ai_comment)

見た目はいい感じですね。

20240711_en_15

しかし、「回答をチェック」ボタンを押すと、画像やテキストエリアが消えて初期状態に戻ってしまいます。

20240711_en_16

実はStreamlitは、ボタンを押すなどウィジェットに何か変更があるたびにアプリが再ロードされます。そのため、「回答をチェック」ボタンを押すと再ロードされ、「画像を生成」ボタンを押した後に表示される内容が消えてしまいます。

そこで、再ロードされても内容を維持できるように、session_stateという機能を使います。通常の変数はアプリが再ロードすると初期状態に戻ってしまいますが、session_stateに保存した内容は再ロードしても値を維持することができます。

Session State - Streamlit Docs

まず、session_stateの状態を初期化します。

# 初期化
if "image_url" not in st.session_state:
    st.session_state.image_url = ""
if "my_answer" not in st.session_state:
    st.session_state.my_answer = ""

続いて、コードを以下のように修正します。

if st.button("画像を生成"):
    # 画像を生成ボタンを押されたら、画像も回答もクリアする
    st.session_state.image_url = ""
    st.session_state.my_answer = ""

    with st.spinner("画像を生成中..."):
        # 画像をセッションに保存
        st.session_state.image_url = get_problem(theme)

# 画像がセッションに存在したら、画像と回答用テキストエリアを表示する
if st.session_state.image_url != "":
    # セッションに保存されている画像を表示する
    st.image(st.session_state.image_url)

    st.write("生成された画像を見て、画像を英文で描写してください。")
    # 回答をセッションに保存
    st.session_state.my_answer = st.text_area("英文の回答", height=200)

    if st.button("回答をチェック"):
        with st.spinner("チェック中..."):
            ai_comment = check_answer(
                image_url=st.session_state.image_url, answer=st.session_state.my_answer
            )
            st.write(ai_comment)

いきなりコード量が増えて戸惑うかもしれません。セッション管理は少しだけ慣れが必要です。しかし、基本的には以下の点をおさえておけばまずは大丈夫です。

  • ずっと維持しておきたい値をsession_stateに保存しておく
  • session_stateに値があるかどうかで表示するウィジェットを制御する
  • 必要なタイミングでsession_stateをクリアする

今回の場合は、「画像のURL」と「ユーザの回答」さえあれば、AIに添削してもらうことができます。そのためその二つをsession_stateに保存しておきます。そして、画像と回答欄が表示されるのは画像のURLが存在しているときです。

このように変更することで、「回答をチェック」を押しても初期状態に戻らず、AIの添削まで表示することができました。

20240711_en_17

コード全体は以下のようになります。

from openai import OpenAI
import os
import streamlit as st

openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)

# 初期化
if "image_url" not in st.session_state:
    st.session_state.image_url = ""
if "my_answer" not in st.session_state:
    st.session_state.my_answer = ""

def get_problem(theme):
    generated = client.images.generate(
        model="dall-e-2",
        prompt=f"{theme}の一場面を写した写真を生成してください。",
        size="512x512",
        quality="standard",
        n=1,
    )

    return generated.data[0].url

def check_answer(image_url, answer):
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"私は画像を英文で描写する問題に取り組んでいます。入力された画像に対して私は{answer}と描写しました。正しく描写できているかどうか、英文に誤りがないかなどをチェックし、日本語で解説してください。",
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": image_url,
                        },
                    },
                ],
            }
        ],
    )

    return response.choices[0].message.content

st.title("画像を英文で描写する問題ジェネレーター")

theme = st.selectbox(
    "テーマを選択してください",
    ["学生生活", "旅行", "仕事", "スポーツ", "自然"],
)

if st.button("画像を生成"):
    # 画像を生成ボタンを押されたら、画像も回答もクリアする
    st.session_state.image_url = ""
    st.session_state.my_answer = ""

    with st.spinner("画像を生成中..."):
        # 画像をセッションに保存
        st.session_state.image_url = get_problem(theme)

# 画像がセッションに存在したら、画像と回答用テキストエリアを表示する
if st.session_state.image_url != "":
    # セッションに保存されている画像を表示する
    st.image(st.session_state.image_url)

    st.write("生成された画像を見て、画像を英文で描写してください。")
    # 回答をセッションに保存
    st.session_state.my_answer = st.text_area("英文の回答", height=200)

    if st.button("回答をチェック"):
        with st.spinner("チェック中..."):
            ai_comment = check_answer(
                image_url=st.session_state.image_url, answer=st.session_state.my_answer
            )
            st.write(ai_comment)

デプロイする

このままでは毎回ローカルで起動する必要があります。そこで、Streamlitの公式クラウドサービスにデプロイしてどこからでもアクセスできるようにしてみます。

その前に、今はOpenAIのAPIキーを環境変数から取得するようになっています。デプロイするにあたってシークレットを登録する手順がありますが、Streamlitのシークレットから値を取得するには独自の書き方があります。

APIキーを取得する部分を以下のように変更します。st.secretsがStreamlitのシークレットから値を取得するための書き方です。

openai_api_key = os.getenv("OPENAI_API_KEY") or st.secrets["openai_api_key"]

また、requirements.txtを作成します。

pip freeze > requirements.txt

作成したコードをGitHubのリポジトリにpushします。

続いてこちらのURLからアカウント登録します。

アカウント登録が終わったら、右上の「Create app」をクリックします。

20240711_en_18

既に作成したアプリをデプロイするので、左の「Yup, I have an app」を選択します。

20240711_en_19

下記のような画面になるので、リポジトリ、ブランチ、ファイルを選択します。App URLはデフォルトのままでも、好きな文字列に変えても大丈夫です。

今回はシークレットの登録があるので、デプロイする前に「Advanced settings」をクリックします。

20240711_en_20

Pythonバージョンとシークレットの設定を行います。

20240711_en_21

デプロイすると、完了するまでポップなローディング画面が表示されます。

20240711_en_22

完了するとWeb画面が表示され、全世界のどこからでもアクセスできるようになりました。(この画像のどのあたりがスポーツなのかよくわかりませんが)

20240711_en_23

ちなみに自分のOpenAIのAPIキーを使用する場合、URLを不特定多数に公開してしまうと思わぬ請求が発生する可能性があるので注意してください。あくまで自分用として使うか、OpenAI側で制限をかけるなどの対策が必要になります。

また、APIキーをアプリ内で設定せずに、都度入力させるような方式もアリかなと思います。

20240711_en_24

この場合のコードは以下になります。なんと1行変更するだけです。

api_key = st.sidebar.text_input("OpenAI API Key", type="password")
client = OpenAI(api_key=api_key)

アプリを修正したい場合は、GitHubのリポジトリにpushすればほぼリアルタイムで反映されます。再デプロイのために何か手動で作業を行う必要はありません。

ちなみにこのクラウドサービスは無料なので、リソース制限があります。また、しばらくアクセスしていないと次回アクセスした際に少し待機時間が入ります。そういった挙動が気になる場合は、Dockerなど他にもデプロイ手段があるので、調べてみてください。

Deployment tutorials - Streamlit Docs

おわりに

使用したAIモデルやプロンプトの内容はお好みでカスタマイズして頂ければと思います。AIなのでたまに変な画像が生成される場合もありますが、英文描写力を鍛えるアプリなので良いのではないでしょうか。

今後もAIを使って語学学習を便利にする仕組みを考案していけたらと思います。

この記事がどなたかの参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.