🙆‍♀️

StreamlitでChatGPT Visionアプリを作成してみよう!

2024/03/16に公開

皆さん、ChatGPT-Visionは使用していますか?
画像は文字よりも多くの情報を持っていて、AIと会話するときにより正確な情報を伝えることが出来ますね!
もしAIチャットボットアプリに画像認識機能があればユーザー満足度向上間違いなしです!

作成したアプリ

OpenAI API keyを入力すると使用出来ます!
https://app-app-wkqyjxhqpibedcpgkggx9n.streamlit.app/

Streamlitとは?

アプリはStreamlitで作成します!Streamlitは、PythonでWebアプリケーションを素早く作成するためのオープンソースライブラリです。データ分析、可視化、機械学習モデルのデモなどに特に適しており、コーディングの専門知識がなくても使いやすいツールです。
https://streamlit.io/

gpt-4-visionのAPI呼び出し方法

執筆時点でgpt-4-vision APIの発表から数か月経っていますが、現時点でAPIのモデル名はgpt-4vision-previewとなっていてまだpreview版のようです。正式版がリリースされるの間近かも!?
https://platform.openai.com/docs/guides/vision

gpt-4-visionのAPI呼び出し方法は以下です!

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
  model="gpt-4-vision-preview",
  messages=[
    {
      "role": "user",
      "content": [
        {"type": "text", "text": "この写真は何ですか?"},
        {
          "type": "image_url",
          "image_url": {
            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
          },
        },
      ],
    }
  ],
  max_tokens=300,
)

print(response.choices[0].message.content)

messagesのurlに画像、textにプロンプトを入れます!
ちなみに画像は下記のような綺麗な風景です。
コードを実行して画像について聞いてみましょう!

以下が回答ですが、ちゃんと答えてくれましたね!

この写真は、草原や湿地を通る木製の歩道が見える自然の風景です。
歩道は、草の高い草原の中を直線に伸びており、散歩や自然観察に適しているように見えます。
空は晴れており、遠くには木々が見えます。自然の美しさと静けさを感じさせるシーンです。

streamlitで実装してみよう!

実際に実装する際にはAgentを設定し、先程のコードをtoolとして呼び出すようにします。
Agentとtoolについては以下の記事で少し解説していますのでこちらもチェックしてみて下さい!
https://zenn.dev/tsuzukia/articles/3fbf91647d50d4

普段はチャットボットとして利用できて必要に応じて画像解析するって感じですね。
https://python.langchain.com/docs/modules/agents/

コード

少し長くなってしまいますが、、、

import streamlit as st
from langchain_openai import ChatOpenAI
from langchain.agents import AgentType, initialize_agent, Tool
from langchain_community.callbacks import StreamlitCallbackHandler
from langchain.memory import ConversationBufferMemory
from langchain.memory import StreamlitChatMessageHistory
from langchain.prompts import MessagesPlaceholder
from langchain.schema.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langchain_core.tracers.run_collector import RunCollectorCallbackHandler
import requests
import base64
import os
from st_img_pastebutton import paste
from io import BytesIO

def encode_image(uploaded_file):
    file_bytes = uploaded_file.getvalue()
    return base64.b64encode(file_bytes).decode('utf-8')

def analyze_image(prompt):
    image_base64=encode_image(st.session_state.uploaded_file)
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {st.session_state.openai_api_key}"
    }

    payload = {
        "model": "gpt-4-vision-preview",
        "messages": [
          {
            "role": "user",
            "content": [
              {
                "type": "text",
                "text": f"{prompt} Let's think step by step to get the correct answer."
              },
              {
                "type": "image_url",
                "image_url": {
                  "url": f"data:image/jpeg;base64,{image_base64}",
                  "detail": "high"
                }
              }
            ]
          }
        ],
        "max_tokens": 4000
    }

    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    response_data = response.json()

    if response_data and "choices" in response_data and len(response_data["choices"]) > 0:
        return response_data["choices"][0]["message"]["content"]
    else:
        return "No analysis available for the image."


def vision():
    st.title("ChatGPT-Vision")
    st.write("**You can analyze images. Please upload an image and ask a question.**")

    clopboard_file = paste(label="paste from clipboard",key="image_clipboard")

    if clopboard_file is not None:
        header, encoded = clopboard_file.split(",", 1)
        binary_data = base64.b64decode(encoded)
        bytes_data = BytesIO(binary_data)
        st.session_state.uploaded_file = bytes_data
        st.image(bytes_data, caption="Uploaded Image", width =500)

    attrs=["messages_vision","agent_kwargs_vision"]
    for attr in attrs:
        if attr not in st.session_state:
            st.session_state[attr] = []
    if "Clear_vision" not in st.session_state:
        st.session_state.Clear_vision = False

    agent_kwargs_vision = {
        "system_message": SystemMessage(content="You are an AI chatbot having a conversation with a human.There may come a time when you are asked a question about photography. The photo has already been uploaded. Use the tool appropriately and answer the question. \
            Instead of directly returning the results obtained from using the tools, you are expected to use those results to respond to the user's questions. DO YOUR BEST TO PROVIDE APPROPRIATE ANSWERS TO USER QUESTIONS!!", additional_kwargs={}),
        "extra_prompt_messages": [MessagesPlaceholder(variable_name="history_vision")],
    }
    msgs = StreamlitChatMessageHistory(key="vision")
    memory = ConversationBufferMemory(memory_key="history_vision",return_messages=True, chat_memory=msgs)
    tools = [
        Tool(
            name="image_analysis",
            func=analyze_image,
            description="Useful when you need to answer a question about an image of some kind. image is already uploaded. Pass the appropriate prompt to ask about IMAGE. \
                Prompt is very important and must be in line with the user's intentions. Instead of directly returning the results obtained from using the tools, you are expected to use those results to respond to the user's questions." ,
            return_direct=False
        )
        ]
    

    for message in st.session_state.messages_vision:
        if not message["role"]=="system":
            if message["role"]=="assistant":
                with st.chat_message(message["role"],avatar = "👁️"):
                    st.markdown(message["content"],unsafe_allow_html=True)
            else:
                with st.chat_message(message["role"], avatar="😊"):
                    st.markdown(message["content"],unsafe_allow_html=True)

    if user_prompt_vision := st.chat_input("Send a message"):

        if st.session_state.openai_api_key == "":
            sac.alert(label='warning', description='Please add your OpenAI API key to continue.', color='red', banner=[False, True], icon=True, size='lg')
            st.stop()

        llm = ChatOpenAI(temperature=0, streaming=True, model="gpt-4-turbo-preview",openai_api_key=st.session_state.openai_api_key)
        agent = initialize_agent(tools, llm, agent=AgentType.OPENAI_FUNCTIONS,verbose=False,agent_kwargs=agent_kwargs_vision,memory=memory)
    
        st.session_state.messages_vision.append({"role": "user", "content": user_prompt_vision})
        with st.chat_message("user", avatar="😊"):
            st.markdown(user_prompt_vision.replace("\n","<br>").replace("$","\\$").replace("#","\\#").replace("_","\\_"),unsafe_allow_html=True)
        with st.chat_message("assistant",avatar="👁️"):
            st_callback_vision = StreamlitCallbackHandler(st.container())
            run_collector = RunCollectorCallbackHandler()
            cfg = RunnableConfig()
            cfg["callbacks"] = [st_callback_vision, run_collector]
            response_vison = agent.invoke(user_prompt_vision, cfg)
            response_vison = response_vison["output"]
            st.markdown(response_vison.replace("\n","  \n"),unsafe_allow_html=True)

        st.session_state.messages_vision.append({"role": "assistant", "content": response_vison})
        st.session_state.Clear_vision = True
                
    if st.session_state.Clear_vision:
        if st.button('clear chat history'):
            st.session_state.messages_vision = []
            response_vison = ""
            msgs.clear()
            memory.clear()
            st.session_state.Clear_vision = False 
            st.rerun()

if __name__ == "__main__":
    if not hasattr(st.session_state, "openai_api_key"):
        try:
            st.session_state.openai_api_key = os.environ["OPENAI_API_KEY"]
        except:
            st.session_state.openai_api_key = ""
    with st.sidebar:
        openai_api_key = st.text_input("OpenAI API Key", type="password")
        if not openai_api_key == "":
            st.session_state.openai_api_key = openai_api_key
        st.write("if you are running the app locally,  \nthere is no need to enter the key  \nif it is already set as an environment variable.")
    vision()

必要に応じライブラリをダウンロードして下さい!

pip install st_img_pastebutton openai langchain streamlit

Streamlitの実行には以下のコマンドをターミナルで実行して下さい。

streamlit run XXXXX.py

大事なポイントについては説明しますね!

tool

以下ではtoolの定義をしています。

tools = [
    Tool(
        name="image_analysis",
        func=analyze_image,
        description="Useful when you need to answer a question about an image of some kind. image is already uploaded. Pass the appropriate prompt to ask about IMAGE. \
            Prompt is very important and must be in line with the user's intentions. Instead of directly returning the results obtained from using the tools, you are expected to use those results to respond to the user's questions." ,
        return_direct=False
    )
    ]

descriptionには長々書いていますが色々試してみてこれが良かったです笑

画像認識関数

ここは先ほど紹介したapi呼び出しですね!

def analyze_image(prompt):
    image_base64=encode_image(st.session_state.uploaded_file)
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {st.session_state.openai_api_key}"
    }

    payload = {
        "model": "gpt-4-vision-preview",
        "messages": [
          {
            "role": "user",
            "content": [
              {
                "type": "text",
                "text": f"{prompt} Let's think step by step to get the correct answer."
              },
              {
                "type": "image_url",
                "image_url": {
                  "url": f"data:image/jpeg;base64,{image_base64}",
                  "detail": "high"
                }
              }
            ]
          }
        ],
        "max_tokens": 4000
    }

    response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
    response_data = response.json()

    if response_data and "choices" in response_data and len(response_data["choices"]) > 0:
        return response_data["choices"][0]["message"]["content"]
    else:
        return "No analysis available for the image."

画像はst.session_stateとして渡しています。
streamlitのチャット機能では画像を渡すことは出来ないのでこのような形にしています。

先ほどは紹介しませんでしたがdetailで分解能を設定できます。low,high,autoを選択できます。
ここはコストに繋がるところですので詳しくは公式ドキュメントを確認して下さい。
また、max_tokensで出力トークンのリミットを設定できます。

画像アップロード

画像はペーストボタンをクリックすることでクリップボードから貼り付けすることができます。

clopboard_file = paste(label="paste from clipboard",key="image_clipboard")

実はこのボタンは自作(カスタムコンポーネント)で、見よう見まねで作成したのでバグがあるかもしれません笑

アップロードした画像はst.imageで表示され、st.session_stateに保存されます。

st.image(bytes_data, caption="Uploaded Image", width =500)

最後に

いかがでしたでしょうか?今回は、ChatGPT VisionとStreamlitを使った画像認識機能付きチャットボットの作り方を紹介しました。画像を通じてより豊かなコミュニケーションを実現することは、ユーザー体験を格段に向上させる可能性を秘めています。

技術は日々進化しており、新しい発見や改善の機会が常にあります。この記事が皆さんのプロジェクトに少しでもインスピレーションを与え、実際の開発や学習に役立つことを願っています。疑問やアイデアがあれば、ぜひ共有してください。一緒に新しい可能性を探求しましょう。Happy coding!

Discussion