まるっとワーク

データ分析・電子工作など気になることを残していきます

langChainを用いた簡単チャットアプリの作成

LangChainは、Large Language Models(LLM)を使ったアプリケーション開発を容易にするためのフレームワークです。LLMと会話をする際に会話履歴の保管しておく機能や、Agentと呼ばれる機能等、自身で色々と書かないと実現できなかったことが簡単にできる所がとても良いです。(詳細以下)
www.langchain.com

今回は、このLangChainとGoogle Gemini API(今は無料で使えるので)を用いて簡単なチャットアプリとpdfを読み込んで、そのpdfの内容に応じて答えを出してくれるアプリを作ったので、その記録を残します。


目次


構成について

LLMとの会話はpython, LangChainライブラリ、アプリ部分はStreamlitライブラリ、実装はGCP CloudRunを使用しています。
基本的な構成は以下ブログで記載と差が無く、app.pyのコードのみ変更します。

dango-study.hatenablog.jp

構成詳細

│─ Dockerfile
│─ requirements.txt
└─ src/
   └─ app.py (WEBアプリのコード)

コード詳細

簡単なチャットアプリ

LangChainのConversationChainの機能を使用しています。
以下はapp.pyのコードの抜粋です。

import streamlit as st
from streamlit_lottie import st_lottie
from langchain.chat_models import ChatVertexAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import AIMessage, HumanMessage, SystemMessage
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.cache import InMemoryCache
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
import requests
from dotenv import load_dotenv
import os

GOOGLE_API_KEY = "自身で取得したAPI KEYを入力"

langchainllm_cache = InMemoryCache()
llm = ChatGoogleGenerativeAI(model="gemini-pro",
                             temperature=0.3, convert_system_message_to_human=True,google_api_key=GOOGLE_API_KEY)

def get_state(): 
     if "state" not in st.session_state: 
         st.session_state.state = {"memory": ConversationBufferMemory(return_messages=True, k=5)} 
     return st.session_state.state 
state = get_state()
print(state)

chain = ConversationChain(
            llm=llm, 
            memory=state['memory']            
        )

def load_lottieurl(url:str):
    r = requests.get(url)
    url_json = dict()
    if r.status_code == 200:
        url_json = r.json()
        return url_json
    else:
        None

load_bot = load_lottieurl("https://lottie.host/dffb1e31-78af-4548-9e7c-30fd1cbbb704/lUvwHha1IZ.json")
col1,col2 = st.columns(2)
with col1:
    st.markdown('')
    st.markdown('')
    st.title(":violet[AI Bot]")
with col2:
    st_lottie(load_bot,height=200,width=200,speed=1,loop=True,quality='high',key='bot')

# Set a default model
if "vertexai_model" not in st.session_state:
    st.session_state["vertexai_model"] = "chat-bison"

# Initialize chat history
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat messages from history on app rerun
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Accept user input
if human := st.chat_input("Please ask assistant"):
    # Add user message to chat history
    st.session_state.messages.append({"role": "user", "content": human})
    # Display user message in chat message container
    with st.chat_message("user"):
        st.markdown(human)
    # Display assistant response in chat message container
    with st.chat_message("assistant"):
        message_placeholder = st.empty()
        with st.spinner("Processing..."):
            #full_response = gen_response(human)
            full_response = chain(human)

        try :
            message_placeholder.markdown(full_response["response"])
            st.session_state.messages.append({"role": "assistant", "content": full_response["response"]})
            print(full_response)
            #print()
        except:
            print("error")

実行結果

デプロイ方法は以下の記載と同じなので、ここでは割愛します。
WEBアプリをクラウドでデプロイ(Cloud Run + Streamlit or Flask) - まるっとワーク

実行結果として、会話が正常に行え、その記録もしっかり残せています。
streamlitは、変数保持に対応が必要なので、会話履歴に対しては保持するための対応をしています。
【Streamlit】Session Stateで変数の値を保持する方法 #Python - Qiita



pdfの内容に応じて答えを出してくれるアプリ

同様にLangChainのConversationChainの機能を使用しています。
ただし、特定の命令(プロンプト)を与えています。
以下はapp.pyのコードの抜粋です。

構成詳細

import streamlit as st
from streamlit_lottie import st_lottie
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain import PromptTemplate
from langchain.cache import InMemoryCache
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.text_splitter import CharacterTextSplitter
from langchain.agents import AgentType, AgentExecutor, ZeroShotAgent, Tool, initialize_agent, load_tools
import requests
from langchain.tools import WriteFileTool
import PyPDF2
import io

GOOGLE_API_KEY = "自身で取得したAPI KEYを入力"

langchainllm_cache = InMemoryCache()
llm = ChatGoogleGenerativeAI(model="gemini-pro",
                             temperature=0.3, convert_system_message_to_human=True,google_api_key=GOOGLE_API_KEY)

def get_state(): 
    if "state" not in st.session_state: 
        st.session_state.state = {"memory": ConversationBufferMemory(return_messages=True, k=5)} 
        st.session_state.state["count"] = 0 
    return st.session_state.state 
state = get_state()
print(state)

tools = load_tools(
    [],
    llm=llm
)

tools.append(WriteFileTool())

chain = ConversationChain(
            llm=llm, 
            memory=state['memory']            
        )

if state["count"] >= 0:
    prompt = PromptTemplate(
        input_variables=["system_input", "document_input", "user_input"],
        template="""System Prompt Here: {system_input}
        Document Input: {document_input}
        User Prompt: {user_input}"""
    )
else:
    prompt = PromptTemplate(
        input_variables=["user_input"],
        template="""
        User Prompt: {user_input}"""
    )
    print("Not First")

system_input = """####Document Input#####の内容を踏まえて、####User Prompt####の命令に従って回答してください。これを厳守してください"""

agent = initialize_agent(
    tools,
    llm,
    memory=state["memory"],
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

def load_lottieurl(url:str):
    r = requests.get(url)
    url_json = dict()
    if r.status_code == 200:
        url_json = r.json()
        return url_json
    else:
        None

def get_pdf_text():
    st.markdown('Managed by: <a href="https://dango-study.hatenablog.jp/">Kazuma Tokuda</a>',unsafe_allow_html=True)
    st.markdown('')
    st.title(":violet[Document AI Search]")
    uploaded_file = st.file_uploader(
        label='Upload your PDF here😇',
        type='pdf'
    )
    if uploaded_file:
        # ファイルをメモリに読み込む
        file_buffer = io.BytesIO(uploaded_file.getvalue())

        # PyPDF2を使用してPDFを読み込む
        pdf_reader = PyPDF2.PdfReader(file_buffer)
        text = ''
        for page in range(len(pdf_reader.pages)):
            text += pdf_reader.pages[page].extract_text() + '\n'
        text_splitter = CharacterTextSplitter(
            separator = "\n\n",
            chunk_size = 100,
            chunk_overlap = 0,
            length_function = len,
        )
        text_list = text_splitter.split_text(text)
        document_list = text_splitter.create_documents([text])

        st.session_state.pdf_text = text_list 
        st.write(text_list)

def page_ask_my_pdf():        
    load_bot = load_lottieurl("https://lottie.host/dffb1e31-78af-4548-9e7c-30fd1cbbb704/lUvwHha1IZ.json")
    col1,col2 = st.columns(2)
    with col1:
        st.markdown('')
        st.title(":violet[Document AI Search]")

    if "pdf_text" in st.session_state:
        pdf_text = st.session_state.pdf_text
        # PDFテキストを使用して何か処理を行う
        # 例: st.write(pdf_text)
        with col2:
            st_lottie(load_bot,height=200,width=200,speed=1,loop=True,quality='high',key='bot')

        # Set a default model
        if "vertexai_model" not in st.session_state:
            st.session_state["vertexai_model"] = "chat-bison"

        # Initialize chat history
        if "messages" not in st.session_state:
            st.session_state.messages = []

        # Display chat messages from history on app rerun
        for message in st.session_state.messages:
            with st.chat_message(message["role"]):
                st.markdown(message["content"])

        # Accept user input
        if human := st.chat_input("Please ask assistant"):
            # Add user message to chat history
            st.session_state.messages.append({"role": "user", "content": human})
            # Display user message in chat message container
            with st.chat_message("user"):
                st.markdown(human)
            # Display assistant response in chat message container
            with st.chat_message("assistant"):
                message_placeholder = st.empty()
                with st.spinner("Processing..."):
                    custom_prompt = prompt.format(system_input=system_input, document_input= pdf_text,user_input=human)
                    print(custom_prompt)
                    print(state["memory"])
                    full_response = agent(custom_prompt)
                    state["count"] += 1
                    try :                
                        #print("full_response", full_response)
                        message_placeholder.markdown(full_response["output"])
                        st.session_state.messages.append({"role": "assistant", "content": full_response["output"]})
                        #print("st.session_state", st.session_state)
                    except Exception as e:
                        st.error(f"An error occurred: {str(e)}")
                        print(e)
    else:
        st.markdown("まずpdfファイルを読み込んでください")

def main():
    selection = st.sidebar.radio("Go to", ["PDF Upload", "Ask My PDF(s)"])
    if selection == "PDF Upload":
        get_pdf_text()
    elif selection == "Ask My PDF(s)":
        page_ask_my_pdf()

if __name__ == '__main__':
    main()


実行結果

pdfを読み込むページと会話をするページを分けています。


pdfを読み込むと読み込んだ結果が表示されます。


会話をするページでは、聞いた内容及びpdfの内容も踏まえて回答してくれています。
ただ、まだ制御が怪しくきれいに出すことができていません・・
また、プロンプトインジェクションにも弱く、なかなか難しいですね・・



まとめ

今回は、LangChainとGoogle Gemini APIを用いて、簡単なチャットアプリを作ってみました。
現在Google Gemini APIは無料で利用できるため、このような試みを行うには絶好の機会です。
LangChainは更新頻度が高く、その進化に大きな期待が持てます。今後も注目していきたいと思います。

できるだけPythonだけでWEBアプリを作る④(Streamlit, Gradio, Dash, Reflex)

「できるだけPythonだけでWEBアプリを作る第4弾」として複数のPython ライブラリの比較を行っていきたいと思います。
各ライブラリの使い方はそれぞれまとめている方がいらっしゃるので、比較結果についてのみをまとめます。

第1弾, 第2弾, 第3弾記事は以下の通りです。
できるだけPythonだけでWEBアプリを作る①(Python + AWS + HTML/Java) - まるっとワーク
できるだけPythonだけでWEBアプリを作る②(Python flask/zappa + AWS) - まるっとワーク
できるだけPythonだけでWEBアプリを作る③(Python Streamlit) - まるっとワーク

目次


PythonでWEBアプリが作れるライブラリについて

丁寧にまとめて下さっている方がいらっしゃいました。(感謝です)
まさかこんなにあるなんて・・・最近まで全然知りませんでした。
これらはPyhtonのみでWebアプリが作れるライブラリであり、html, cssを書く必要があるFlask, Djangoは入っていませんね。
zenn.dev

ご参考↓


今回はまとめて下さっているライブラリの中で、GitHub star数が相対的に多めである4つ(Streamlit, Gradio, Dash, Reflex)の比較を行いました。

ライブラリの比較結果

比較検討のために作成したコードは、以下 Githubで公開をしています。
github.com

結果はかなり所感が入っているので、ご注意下さい。
まとめると、以下の通りの使い分けになると思っています。
とりあえずシンプルなUIでよい!素早くアプリを出したい!:Streamlit, Gradio
UIのカスタマイズをしたい!オリジナリティを出す必要がある!:Dash, Reflex


比較結果を表でまとめています。

観点 説明 Streamlit Gradio Dash Reflex
用途 フレームワークの主な利用シナリオ。 データ分析や機械学習モデルの可視化、プロトタイピング、簡単なWebアプリケーション 機械学習モデルのデプロイ、モデルの入力と出力の簡単な設定 データダッシュボード、高度な可視化、企業向けアプリケーション リアルタイムアプリケーション、コラボレーション、高度な対話型アプリケーション
インタラクティブ ユーザーとの対話性能力(反応性)。 高い 中程度 高い 高い
UIカスタマイズ ユーザーインターフェースの外観と振る舞いを調整する能力。 限定的(デフォルトのウィジェットをカスタマイズ可能) 限定的(デフォルトのウィジェットをカスタマイズ可能 カスタマイズ可能(HTML / CSS / JavaScriptを使用してUIをカスタマイズ) カスタマイズ可能(HTML / CSS / JavaScriptを使用してUIをカスタマイズ)
シンプルさ 使用の簡便さ。 高い(シンプルなコードでアプリケーションを構築できる) 高い(簡潔なAPIでモデルデプロイが可能) 中程度(高度なカスタマイズが必要な場合がある) 中程度(カスタマイズが必要な場合がある)
コミュニティサポート オンラインコミュニティのサポートとリソースの利用可能性。 高い 中程度 高い 中程度
ドキュメンテーション フレームワークの公式ドキュメントとチュートリアルの品質と充実度。 良い 良い 良い 良い


それぞれ特徴がありますが、シンプルなUIで良いからとりあえずアプリを出したいということであれば、Streamlit, Gradioという選択になり、後はカスタマイズの希望に応じて、Dash, Reflex, Flask, Django等・・・という感じかなと

GUI比較(ご参考程度ですが)

同じ目的のアプリを作成した時の見た目の差の比較です。
ただし、見た目についての調整はほとんどしておらず、、、最初からある程度デザイン性があるStreamlit, Gradio, Reflexの方が見た目は良いかもですね。

Streamlit


Gradio


Dash


Reflex



まとめ

簡単ですが、以上です。
PythonだけでWEBアプリを作ることができるライブラリがこんなにあるなんて、知りませんでした。
これだけあると、どれを使えば良いのか分からなくなってくるので、特徴などを比較しながら適切に選択していきたいなと思います。

できるだけPythonだけでWEBアプリを作る③(Python Streamlit)

できるだけPythonだけで・・・と進めてきましたが、画期的なフレームワークの存在最近まで知らず・・・
「できるだけPythonだけでWEBアプリを作る第3弾」としてPython ライブラリのStreamlitとStreamlit Cloudを使用した方法をまとめていきます。
この方法は他の方も多くまとめているので、備忘録として簡単なフローだけを示しています。

第1弾, 第2弾記事は以下の通り。
dango-study.hatenablog.jp
dango-study.hatenablog.jp

目次


Streamlitでできること

  • PythonだけでWebアプリが作れる
  • フロントエンド(UI)の開発スキルが無くても簡単にUIを作ることができる
  • 作成したアプリをStreamlit Cloudに(無料)デプロイできる

デプロイ方法は、Streamlit Cloud意外に色々ありますが、Streamlit Cloudではセキュリティーが担保できないので、機密データを処理したい時などは、ローカルサーバーやセキュリティ保護されたサーバーでの運用が望ましいです。

Streamlitを用いたWEBアプリ作成

開発環境

[ローカル]
Microsoft Windows 10 Pro
Python 3.9.0

ライブラリのインストール

以下を実行して必要なライブラリをインストールします。

$ pip install streamlit

アプリ構成

基本的な構成は、「処理等を記載した.pyファイル」「必要ライブラリとバージョン情報を記載したrequirements.txt」の2つの構成になります。

@@@.py
requirements.txt

コード作成

任意の名前の.pyファイルには、最低限streamlitのモジュールインポート記載をしておけば、実行確認ができます。

$ import streamlit as st 

ローカルで実行/確認

コンソールで以下コードを実行して、
エラーが出なければ「http://localhost:8555」にアクセスして実行結果を確認できます。

$ streamlit run @@@.py

→.pyファイルの記載がstreamlitのモジュールインポート記載のみであれば、白紙のページが表示されるだけです。

コードの書き方などは、ありがたいことに・・先人の皆様のページで詳しく説明されているので割愛します。

  • 参考コード(私)

https://github.com/Mya-mori/streamlit_csv/blob/main/test.py

  • 参考ページ①

Streamlit入門+応用 ~ データ分析Webアプリを爆速で開発する - Qiita

  • 参考ページ②

PythonでDX「Streamlit」簡単ダッシュボード(前編) | コードファミリー

  • 参考ページ③

Streamlitで手軽にWebアプリ開発 - アルファテックブログ

アプリをStreamlit Cloudにデプロイ

Streamlit Cloudにサインイン

https://share.streamlit.io/signup
Googleアカウント、Githubアカウント、メールアドレスのいずれかを利用してアカウントを作成します。

Githubアカウント連携

Settingsをクリックし、Connect Github accountをクリックして、Githubと連携する。
(Githubアカウントがない場合は、作成する)

GithubリポジトリにWebアプリのソースコードをアップロード

作成したソースコードGithubにアップロードします
アップロードするファイルは、最低限以下構成のファイル

@@@.py
requirements.txt

requirements.txtは以下コードを実行することで、出力することができる。
必要最低限の情報でないとエラーが出る場合がある為、余計なライブラリ情報は削除する。(ライブラリが多すぎることが原因のエラーが出ました)

$pip freeze > requirements.txt

アプリのデプロイ

Streamlit CloudにStreamlitで作成したWebアプリをデプロイします。
Streamlitにログインして、"New app"と記載されたボタンを押す。

以下の通り設定をして、デプロイボタンを押すことで、デプロイされる。

何か問題があれば、エラーが吐き出されます。

動作確認

デプロイされるアプリURLにアクセスすると、デプロイしたアプリの動作確認ができます。
簡単にデプロイして利用できるのがStereamlit Cloudを用いた利点ですね。
参考までに私が作成したアプリは以下↓

https://webapplicationsample1-rwb1ydqchif.streamlit.app/
システム同定シミュレーターアプリ】
csvデータを入力して、線形システムのシステム同定シミュレーションができます。
Githubアイコンを押すとコードも参照頂けます。

まとめ

今回は、前回とは違う方法でWEBアプリを作ってみました。
正直、セキュリティーとかなんやら何も気にしなくてよいのであれば、この方法が一番簡単だなと思いました。今までの努力は・・笑
次は、こういったフレームワーククラウドを組み合わせてかつ、セキュリティも考慮したアプリを作成していこうと思います。

できるだけPythonだけでWEBアプリを作る②(Python flask/zappa + AWS)

「できるだけPythonだけでWEBアプリを作る第2弾」は、第1弾から少し構成を変えて、拡張性を考慮したやり方で作っていきます。
今回はZappaというフレームワークを使っており、様々なAWSの設定が簡単になる反面、裏で何をやっているのかが完全に理解できなかったので、とりあえずシステムの作成フローをこのページにまとめて、掘り下げて分かったことは追ってまとめようと思います。
第1弾(前回)記事は以下の通り。
dango-study.hatenablog.jp

目次


WEBアプリをクラウド(AWS)で作成するためのアプローチ

前回はとりあえずWEBアプリを作る!という感じだったので、特に気にしていなかったのですが、本来はどんなアプローチ方法がとれるのかを踏まえた上で適切に選択すべきなので、ここで一旦まとめようと思います。調べた限り、(AWS)サービスの組み合わせ方で言うと、かなり色々な方法があったので、主観ですが大きなくくりでまとめていきます。

FaaS(Function as a Service)サービスを使うアプローチ[AWS Lambda + API Gateway等]

FaaSとは「Function as a Service」の略で、サーバーレスでアプリケーション開発ができるクラウドサービスのこと。
AWS Lambda等のインスタンスを返してサーバーを任意のタイミングで利用するパターンで、サーバー管理はサービスベンダーが行うため、その費用などを抑えることができる。

CaaS(Container as a Service)サービスを使うケース[ECS + Fargate等]

CaaSとは「Container as a Service」の略で、コンテナ化をすることで環境に依存しない開発ができるクラウドサービスのこと。
CaaSサービスであるECSはAmazon EC2インスタンスを用いてDockerコンテナ管理することができるが、Amazon EC2のような仮想サーバーの管理は不要なアーキテクチャを構築できる。

IaaS(Infrastructure as a Service

IaaSとは「Infrastructure as a Service」の略で、サーバやストレージなどのインフラを提供するサービスのこと。
仮想サーバーを使用できることから開発自由度は上がるが、サーバー管理が必要であること(作業が増える)、コストが他と比較して高い。

LambdaとECS、EC2を様々な観点で比較結果は以下が詳しくまとめられていました。感謝です
EC2 vs ECS vs Lambda:APIのサービングの観点から | Hakky Handbook

つまり、使い分けは、以下の通りと理解しました。
現時点ではLambdaでできる範囲で問題がなさそうなので、コストを重視してLambdaを使う構成で対応します。

おすすめサービス
開発における制限(実行時間やライブラリ利用可否)を重視する場合 ECSかEC2
コストを重視する場合 Lambda


アーキテクチャについて

(おさらい)前回作ったアプリのアーキテクチャ

以下画像のような構成で、Amazon S3に保存したHTMLファイルにアクセス頂き、HTMLページのボタンを押すと、JavaScriptからAmazon API GatewayにHTTPリクエストが送信され、 AWS Lambdaを動かして、計算した結果をJavaScriptでHTMLに上書きするといった動きをします。

今回作るアプリのアーキテクチャ

ここが完全に理解できていないので、現状記載ができない(後述のZappaが理解できていない)。なんで、前回と同じ構成で作らないのか?という疑問が出てくるが、今回はFlaskというPythonのWebアプリケーションフレームワークで作成したWebアプリケーションをAWSでデプロイしたいから、が理由になります。今後も想定すると、1からすべて作る前回の方法よりもフレームワークを使った方がよさそうだなと・・・また、今回はFlaskのWEBアプリケーションをAWSにデプロイするのに、Zappaというフレームワークも使っています。ZappaはAWS LambdaとAPI Gatewayを使って、FlaskやDjangoなどのPython WEBアプリケーションをデプロイすることができます。AWS上の設定などを自動で設定して、構成してくれるのが便利なようです。このような理由から、こちらを使いました。
AWS マネジメントコンソールのAWS Lambda画面では、以下のような画面になっていますが、全体の構成としては、AWS Lambda/EventBridge/API Gatewayの構成になっている?この辺りから怪しいのですが、アクセスするURL(WEBアプリ)はAPI Gatewayで作成するHTTPエンドポイントで、HTTPリクエストをAWS Lambda関数に転送して実行していると思っています。ただ、EventBridgeでAWS Lambdaを定期実行する必要があるのでしょうか?Zappaを使うと、インターフェースの設定を勝手にやってくれるのが便利なのですが、どう設定をしたのかを勉強したい人にとってはつらいですね・・・

分かっていない部分も多いですが、、じゃんけんゲームのアプリを作っていきます。

ちなみに、その他の同じような構成(FaaSサービスを使うケース)例として、以下を紹介しておきます。
AWS での開始方法 | AWS Lambda、Amazon API Gateway、AWS Amplify、Amazon DynamoDB、および Amazon Cognito を使用してサーバーレスウェブアプリケーションを構築する

開発環境

[ローカル]
Microsoft Windows 10 Pro
Python 3.9.0
[AWS]
AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon S3

開発手順

  1. 前準備
  2. アプリ作成(コード作成)
  3. コード実行(動作確認)


実装

前準備

AWS CLIを使えるようにする(省略)

ZappaではAWS CLIを使用する為、使えるようにしておく必要がある。
こちらは色々なページで記載があるので省略します。
AWS CLIの設定(以下)をするところまで対応します。

$ aws configure

↓参考ページ
AWS CLIを利用するメリットと導入方法 | TOKAIコミュニケーションズ AWSソリューション

仮想環境の導入

Zappaを使うには仮想環境の用意が必須となる為、その手順を説明します。
手順は以下の通りコマンドとともに示します。

1. 仮想環境を作る venvモジュールを使用する。pyenvモジュールを使うケースも多いみたいであるが、Windows OSだと問題がありそうなのでこっちを使う(私はpythonバージョン3.9.0が入っていたので、Python 3.9.0の仮想環境ができる)
$ python -m venv <好きなファイル名>
$ cd <上記コマンドで作成したファイルパス>

2. 仮想環境に入る
$ Scripts\activate

必要モジュールのインストール

Zappaでは仮想環境の用意が必須で、アプリを構成するコード類と合わせて仮想環境のモジュールもzip化してAWS Lambdaにアップロードするので、不要なモジュールを入れすぎても負荷を掛けそうなので、最低限のモジュールをインストールする。
ここからの作業は仮想環境に入った状態で対応します。

$ pip install flask
$ pip install zappa

私の環境では、この後のZappaモジュール実行時にurlib3に関するエラーが出た為(バージョン指定)、エラーに従って以下の通り対応した。
$pip uninstall urllib3
$pip install "urllib3>=1.25.4,<1.27"

Zappaの設定

WEBアプリケーションのディレクトリを作成して、その中で初期化コマンドを実行します。

適当にフォルダを作成する
$ mkdir 適当な名前
$ cd <上記コマンドで作成したファイルパス>

Zappaの初期化を行う
$ zappa init


適当に設定すると(後から変えれるので全てEnterでも良い)、現在のディレクトリにzappa_settings.jsonが作成される


アプリ作成(コード作成)

やっとアプリの作成に移り、じゃんけんゲームのアプリを作っていきます。
htmlは2種類、初期画面/じゃんけん結果画面の2つであり、コードを含めたもろもろの構成は以下の通りです。

<flask_sample> 
 ├ <templates>  
 │  ├ home.html 初期画面 
 │  └ result.html じゃんけん結果画面
 └ __init__.py   
my_app4.py アプリで最初に実行されるファイル
requirements.txt 必要モジュールなどを記載したtxt
zappa_settings.json zappa設定ファイル

WEBページの作成

初期画面/じゃんけん結果画面は以下のコードで構成しています。

home.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>じゃんけんゲーム</title>
	<style>
		body {
			font-family: Arial, sans-serif;
			text-align: center;
		}
		h1 {
			font-size: 32px;
		}
		form {
			margin-top: 32px;
			display: flex;
			flex-direction: column;
			align-items: center;
		}
		form label {
			font-size: 20px;
			margin-bottom: 8px;
		}
		form input[type="radio"] {
			margin-right: 8px;
		}
		form button {
			margin-top: 16px;
			font-size: 24px;
			padding: 8px 16px;
			border: none;
			background-color: #1E90FF;
			color: #FFFFFF;
			border-radius: 8px;
			cursor: pointer;
		}
		form button:hover {
			background-color: #4169E1;
		}
	</style>
</head>
<body>
	<h1>じゃんけんゲーム</h1>
    <form action="{{ url_for('play') }}" method="POST">
		<label><input type="radio" name="choice" value="グー" required>グー</label>
		<label><input type="radio" name="choice" value="チョキ">チョキ</label>
		<label><input type="radio" name="choice" value="パー">パー</label>
		<button type="submit">決定</button>
	</form>
</body>
</html>

ハマったところとして、画面遷移が最初はうまくいきませんでした・・・("forbidden 403"が表示)
該当箇所は、

(修正後です)の個所で、初期はパスを直書きしていたことが問題でした。Flaskのurl_for関数を使って関数とURLを紐づけてアクセスできるように変えています。url_for('play')と書くことで、関数:playが実行されます。詳細は以下参照しました。
【Python】Flask url_forについて | しげっちBlog


result.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>じゃんけんゲーム - 結果</title>
	<style>
		body {
			font-family: Arial, sans-serif;
			text-align: center;
		}
		h1 {
			font-size: 32px;
			margin-top: 48px;
			margin-bottom: 32px;
		}
		.result {
			font-size: 24px;
			margin-bottom: 16px;
		}
	</style>
</head>
<body>
	<h1>結果</h1>
	<div class="result">あなたの選択: {{ user_choice }}</div>
	<div class="result">コンピュータの選択: {{ computer_choice }}</div>
	<div class="result">結果: {{ result }}</div>
	<a href="{{ url_for('home') }}">もう一度プレーをする</a>
</body>
</html>

{{}}でくくられた箇所が変数であり、pythonコードから返された結果が表示されます。

pythonコードについて

pythonコードは2ファイル(__init__.pyだけの1ファイルだけでも良かったが・・)に分かれていて、最初にmy_app4.pyが実行される設定にしています。
コードは以下の通りです

my_app4.py

#----------
#Flaskアプリケーション実行ファイル
#----------

from flask_sample import app

if __name__ == "__main__":
    app.run()

__init__.pyを実行するだけの役割ですね


__init__.py

#----------
#Flaskアプリケーション立ち上げ後に呼び出されるファイル
#my_app4.py→__init__.py
#----------

#Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask,render_template, request, url_for
import random

#Flaskオブジェクトの生成
app = Flask(__name__)

#「/」へアクセスがあった場合に、「home.html」を返す
@app.route("/")
def home():
    return render_template("home.html")

@app.route("/result", methods=["POST"])
def play():
    # ユーザーの選択を取得
    print("def_play_start")
    user_choice = request.form["choice"]
    
    # コンピュータの選択をランダムに決定
    choices = ["グー", "チョキ", "パー"]
    computer_choice = random.choice(choices)

    print("chiices", choices)
    
    # 勝敗を判定
    result = ""
    if user_choice == computer_choice:
        result = "引き分け"
    elif (user_choice == "グー" and computer_choice == "チョキ") or (user_choice == "チョキ" and computer_choice == "パー") \
        or (user_choice == "パー" and computer_choice == "グー"):
        result = "勝ち"
    else:
        result = "負け"
    
    # 結果を表示
    return render_template("result.html", user_choice=user_choice, computer_choice=computer_choice, result=result)

コチラのコードがメインとなり、初期アクセスでは関数: homeが実行され、後述のhome.htmlが表示されます。

コード実行(動作確認)

アプリのデプロイ

作ったアプリは、zappa_settings.jsonファイルがあるディレクトに移動して、以下コードでAWSにデプロイします。

デプロイする
$ zappa deploy default ←defaultは自分が設定した値

一度デプロイをして、アップデートをしたい場合は以下を実行
$ zappa update default ←defaultは自分が設定した値

アプリケーションを削除
$ zappa undeploy default ←defaultは自分が設定した値

デプロイ時などのエラーを見たい場合
$ zappa tail

デプロイが完了すると、以下文字が出力され、URLが吐き出される。

Your updated Zappa deployment is live!: https:/*******************


動作確認

吐き出されたURLにアクセスする。


決定をクリックする

どこからでもアクセスができるURLでじゃんけんゲームができるようになりました。

まとめ

今回は、前回とは違う構成でWEBアプリを作ってみました。
フレームワークを使うと、便利なのですが若干ブラックボックスな部分が出てくるので、理解には少し時間がかかりそうです・・・
ただ、Flaskを使ったアプリを展開できるようになったので、もう少し複雑なものを作ってみたいと思います。

できるだけPythonだけでWEBアプリを作る①(Python + AWS + HTML/Java)

最近は生成AIを応用して色々と作ってみたりしていたのですが、著作権問題など色々な問題が話題になっているので、少し様子見で他のことを一度やってみようと思います。
今回は「WEBアプリを作ろう」ということで、作るまでの過程をまとめます。
ちなみに、私はPython以外の言語があまり扱えないので、Pythonを中心としてWEBアプリを作っています。
今回は初回ということで、簡単なものを作っており、次はもう少し複雑なものにもチャレンジしたいです。「作成したアプリ: タイヤサイズからタイヤ外径を計算するアプリ」

WEBアプリを作ろうとしたきっかけ

何か機能を作成した後、その機能をどなたかに使ってもらおうとすると、コードを渡して各々の環境で使ってもらうというやり方になるのかなと思うのですが・・・環境依存の問題があり、うまくいかないことも多いです。また、"不特定多数の人に気軽に使って貰えるものを作る"という目標達成のためにもWEBアプリを作れるようになることが大切かなと思っています。

目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon S3
フロント側はHTML/Javaで構築し、バックエンド側はAWSで構築する
→複雑なところはAWS+Pythonでという戦略

(追記)アーキテクチャ

開発手順

  1. 前準備
  2. Amazon S3を使用する環境設定
  3. AWS Lambdaでコード実行(動作確認)

実装

前準備(AWS環境を整える)

AWS Lambda関数の作成

AWSコンソールにログインして、Lambdaの画面を開き、関数->関数作成を押下する。
以下のような画面が出るので、関数名は適当に記入、ランタイムは"Python3.9"を設定する。

関数

API Gatewayの作成/設定

苦戦したので詳しく書きますが、まだ完全に理解できていないため、補足は追記予定。

Lambdaの関数画面で"トリガーを追加"個所をクリックして、ウィンドウを開く。
ウィンドウが開いたのち、"API Gateway"を選択すると以下画像のような設定画面が現れるので、APIタイプ REST API/セキュリティは"開く"を選択して設定する。

詳細設定

上記の通り作成すると、先程の関数ウィンドウのトリガー欄に、API Gatewayが作成される。

API Gateway


API Gatewayの名前をクリックして、詳細を設定する。
以下画像では、"Resources"欄に"/POST"という1つしか存在しないが、当初は"ANY"が存在している。
"ANY"はすべてのHTTPメソッドをサポートしており、色々使えそうであるが・・使い方が理解できていないので、一旦違う方法で対応。"ANY"メソッドは削除して"GET"メソッドを作成する。GETメソッドは、指定したリソースのデータを返すメソッドで、HTML上でJavaを呼び出し、API Gateway経由でAWS Lambdaを呼び出してその値をJavaに返す為に使用する。
作成画面は以下の通りで"Lambda 関数"と記載の個所は、自身の関数名を入力する

作ろうとしているシステムは、HTML/javaからのAWS API GatewayへのGETリクエストをAWS Lambdaに渡して、その結果を返すというもので、HTMLの入力値をLambdaに渡す必要があります。まずは、その渡す部分の設定をしていきます。

統合リクエストを選択して、"マッピングテンプレート(jsonパラメータ)の追加"を行う


json形式でAws lambdaに値を渡す為、Context-Typeにapplication/jsonを追加して、テンプレートを追加する。

{
    "input1": "$input.params('param1')",
    "input2": "$input.params('param2')",
    "input3": "$input.params('param3')"
}


HTML/javaからは3つの変数(param1,param2,param3)が渡され、それらをlambdaに渡します。
lambda側からは、それぞれの変数をevent["param1"], event["param2"] , event["param3"] で取り出すことができます。

  • API Gatewayの設定③(オリジン間リソース共有 (CORS)設定)

これに関しては理解が追いついておらず、詳しい説明は別サイトを参照した方が良い。
今回のプログラムを実行しようとするとブラウザのCORSというポリシーによるエラーが発生する。このエラーを回避するために、Aws lambdaから返ってきた値をHTML/javaに返す際に、アクセス許可をするためのヘッダーを追加する。

developer.mozilla.org

統合レスポンスを選択して、"レスポンスヘッダーの追加"を行う


以下の通り、"*"を入れることで、アクセス許可を付与

  • API のデプロイ

"Resources"の横の"Actions"欄から、Deploy APIを選択することで、APIをデプロイ(設置)することができる。
URLに載っています。
デプロイ時のStage名などは、任意に設定する。

WEBページの作成/Amazon S3への保管

Amazon S3の設定について[省略:別ページで紹介]

dango-study.hatenablog.jp

WEBページ(HTML)の作成

HTMLの作成方法は以下を参考にして作成した。
基本的なフォーム -- ごく簡単なHTMLの説明

以下コードの"script src="の欄に、次で紹介するjavascriptのオブジェクトURL(S3に保管した)を入力する。コードはメモ帳などに入力後、"@@.html"の形式で保存しておく。

<!DOCTYPE html>
<heml lang="ja">
<head>
    <meta charset="utf-8">
    <title>Webアプリケーション sample1</title>
</head>
<body>

<article>
    <h1>Webアプリケーション タイヤ外径計算機</h1>
    <section>
        <p>タイヤサイズ情報を入力すると、タイヤ外径値を返します</p>
        <table>
        <tr>
        <th class="hissu"><label>タイヤサイズ</label></th>
        <td><textarea id="input_textfield1" name="input" rows="2" cols="10"></textarea></td>
        <th>/</th>
        <td><textarea id="input_textfield2" name="input" rows="2" cols="10"></textarea></td>
        <th>R</th>
        <td><textarea id="input_textfield3" name="input" rows="2" cols="10"></textarea></td>
        </tr>
        </table>
        <br>
        <input type="submit" value="計算結果" onclick="send_message();">
        <script src="<javaスクリプトのオブジェクトURLを記入>"></script>
        <h2>出力結果</h2>
        <table>            
        <tr>
        <th id="output_label1">ここに出力されるよ</th>
        <th>[mm]</th>
        </tr>
        </table>
        <p></p>
    </section>
</article>
</body>
</heml>

WEBページ(java)の作成

javaの作成方法は以下を参考にして作成した。
JavaScriptでAPIを呼び出す方法を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

以下コードの" request_url ="の欄に、AWS lambda 関数のオブジェクトURLを入力する。
コードはメモ帳などに入力後、"@@.js"の形式で保存しておく。
HTML/javaからAWS lambdaに変数を渡すことになるが、HTTPリクエストのパラメータリクエストは以下のように記載する。
↓HTTPリクエスト(パラメータあり)

https://(AWS lambda関数)?param1=値&param2=値&param3=値
#####"値"には実際に渡す引数値が入る。#####


コード

const send_message = () => {
  // URLを作成
  let input_label1 = document.getElementById("input_textfield1");
  let input_label2 = document.getElementById("input_textfield2");
  let input_label3 = document.getElementById("input_textfield3");
  var parameter1 = input_label1.value;
  var parameter2 = input_label2.value;
  var parameter3 = input_label3.value;
  parameter1 = parameter1.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter2 = parameter2.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter3 = parameter3.replace(/\r?\n/g, '\\r\\n'); // 改行コードを入れるとAWSでの処理が怪しかったので、文字列に置換している(TODO:改善)
  parameter1 = encodeURI(parameter1);
  parameter2 = encodeURI(parameter2);
  parameter3 = encodeURI(parameter3);
  console.log(parameter1);
  console.log(parameter2);
  console.log(parameter3);

  request_url = "<AWS lambda関数のオブジェクトURLを記入>?";
  request_url = request_url + "param1=" + parameter1 + "&" + "param2=" + parameter2 + "&" + "param3=" + parameter3;
  console.log(request_url);

  // リクエストオブジェクトの作成"
  var request = new XMLHttpRequest();
  request.open('GET', request_url, true);

  // リクエストが成功したときに呼ばれる関数
  request.onload = function () {
    var json_data = this.response;
    var return_message1 = JSON.parse(json_data)["body"];

    let output_label1 = document.getElementById("output_label1");
    output_label1.innerText = return_message1;
  };

  request.send(); // URLリクエストを送信する
}


AWS lambdaから返ってきた値は、以下の通りに受け取って、HTMLの更新を行う。

  // リクエストが成功したときに呼ばれる関数
  request.onload = function () {
    var json_data = this.response;
    var return_message1 = JSON.parse(json_data)["body"];

    let output_label1 = document.getElementById("output_label1");
    output_label1.innerText = return_message1;
  };

Amazon S3にウェブページを保管

WEBページHTML/JavaをそれぞれS3に保存する。
HTMLファイルのオブジェクトURLがWEBアプリのアクセスURLとなる
フロントエンド部分をHTMLで構成している為

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
(こちらがフルのコードとなります)

import json

def lambda_handler(event, context):
    # TODO implement
    param1 = event['input1']
    param2 = event['input2']
    param3 = event['input3']

    try:
        if str.isnumeric(param1):
            overall_diameter_of_tyre = float(param1)*float(param2)/100*2+float(param3)*25.4
        
        return {
            'statusCode': json.dumps(200),
            'body': json.dumps(overall_diameter_of_tyre)
        }
    except Exception as e:
        raise e
        return {
            'statusCode': json.dumps(400),
            'body': json.dumps("Error")
        }


コード解説

HTML/javaからは3つの変数(param1,param2,param3)が渡され、それらをlambdaに渡します。
lambda側からは、それぞれの変数をevent["param1"], event["param2"] , event["param3"] で取り出すことができます。

param1 = event['input1']
param2 = event['input2']
param3 = event['input3']


lambdaで処理した結果は、関数:lambda_handlerの戻り値でjson形式で返す

        return {
            'statusCode': json.dumps(400),
            'body': json.dumps("Error")
        }

Java側の値の受け取り方は、上の記述の通り

実行結果

値を入力した後に"計算結果"ボタンを押すと、出力結果が表示されました。

まとめ

Python以外が詳しくない私でもWEBアプリを作ることができました。
かなり単純なアプリで、これだったらAWS+Pythonの必要ないのでは・・と思われるかもしれないので、次はもう少し複雑なものを作っていきたいと思います。

LINEの質問(Text)に対して音声で返してもらう(ChatGPT + AWS Polly + LINE)

AWSサービス(Lambda + API Gateway + AWS Polly)とLINE、ChatGPTとを連携させて、LINEで質問した内容の返信を音声で受け取るシステムを作った時のメモを残します。
最終目標として、RVC(Retrieval-based-Voice-Conversion)やbark(以下)といった技術を使用して、流ちょうなしゃべり方、特定のキャラクターのしゃべり方への音声変換して使われることを目標としています。
とりあえずは、その前段階の作成記録として、順々にまとめていきます。
github.com


目次

使用環境

AWS Lambda ランタイム python3.9
AWS API Gateway
Amazon Polly
Amazon S3
・LINE (developers)
・ChatGPT API

開発手順

  1. 前準備(過去ブログに記載されている為 省略)
  2. Amazon Polly + Amazon S3を使用する環境設定
  3. AWS Lambdaでコード実行(動作確認)

実装

前準備[省略:別ページで紹介]

これらの手順は過去のブログ(以下)で説明をしているので、そちらをご参照ください。
具体的な作業は、AWS API Gateway + LINE API + ChatGPT APIを使うための準備になります。
dango-study.hatenablog.jp
dango-study.hatenablog.jp

Amazon Polly + Amazon S3を使用する環境設定

Amazon Polly はAWSのサービスの一つで、深層学習技術を使用し、人間の声のような音声を合成します。Text-to-Speech (TTS)の方法は他にも色々ありますが、他の方法をAWSで実装するよりも既存のものを使った方が安いし早いと思ったので、こちらのサービスを使いました。

今回は、このサービスにChatGPTの返答結果(Text)を入れて、音声ファイルを出力させます。
この音声ファイルをLINEで送る為に、一度どこかに保管が必要であり、その保管場所として、Amazon S3を使用します。

Amazon S3の設定について
  1. AWS コンソールにサインインして、Amazon S3にアクセスをします
  2. S3のバケットを作成します



  • 各種設定についての特記事項


 LINEにデータを渡すための方法として、パブリックアクセスができるような設定にしています。
 S3コンソールを使用したバケット作成時に、パブリックアクセスのブロック設定の無効化に s3:PutBucketPublicAccessBlock 許可が必要となる為、IAMのポリシーに追加する必要がある。

  • パブリックアクセスができるのかどうかの確認


試しに、buffer.mp3というファイルをアップロードして、そのファイルにアクセスできるかどうかを確認する。
ファイルのタブをクリックして"URLをコピー"を押すと、ファイルのアクセスパスを入手できる。
そのパスにブラウザでアクセスをした時に、ファイルをダウンロードをすることができれば、パブリックアクセス設定ができている。

Amazon Pollyの設定について
  1. AWS LambdaからAmazon Pollyにアクセスができるようにロールを追加

ロールを追加したら、あとはコードを書くだけ。
以下を参考にして、コードを作成する。
開始方法 - Amazon Polly | AWS

AWS Lambdaでコード実行(動作確認)

コード作成

Lambda関数では、以下コードを作成/実行する。
(こちらがフルのコードとなります)

import json
import urllib.request
import os
import openai 
from linebot import LineBotApi
from linebot.models import TextSendMessage, AudioSendMessage
import boto3
from botocore.exceptions import ClientError
  
#######LINE Bot関係
# 環境変数からLINE Botのチャネルアクセストークンを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
# チャネルアクセストークンを使用して、LINE BotのAPIインスタンスを作成
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

#######ChatGPT
openai.api_key = os.environ["Openai_ACCESS_TOKEN"]
#Token数
MAX_TOKENS = 150
#モデルを指定
MODEL_ENGINE = "gpt-3.5-turbo"

#######Dynamo DB

def get_system_prompts():
    try:
        # Reverse messages
        system_prompts = {"role": "system", "content": "質問を返してください"}
                
        # Join the lists
        return system_prompts

    except Exception as e:
        # Raise the exception
        raise e

def use_chatgpt(messages):
    try:
        response = openai.ChatCompletion.create(
            model=MODEL_ENGINE,
            messages=messages,
            max_tokens=MAX_TOKENS
        )
        answer = response.__dict__["_previous"]["choices"][0]["message"]["content"]
        return answer
    except Exception as e:
        # Raise the exception
        raise e

def reply_message_for_line(reply_token, completed_text, original_content_url):
    #LINEで入力されたテキストは ['events’][0]['message’]['text’] に入っています。
    #また、受信したメッセージに対してリプライを行うには、replyTokenの値を使用します。
    try:
        # Reply the message using the LineBotApi instance
        print(original_content_url)
        #テキストで返す場合は、以下を使用する
        #LINE_BOT_API.reply_message(reply_token, TextSendMessage(text=completed_text))
        #音声で返す場合は、以下を使用する
        LINE_BOT_API.reply_message(reply_token, AudioSendMessage(original_content_url=original_content_url, duration=30000))

    except Exception as e:
        # Raise the exception
        raise e

def create_completed_text(line_user_id, prompt_text):

    # Create the list of a current user prompt
    current_prompts = {"role": "user", "content": prompt_text}
    
    system_prompts = get_system_prompts()
    
    messages = [system_prompts] + [current_prompts]
    
    completed_text = use_chatgpt(messages)
    
    return completed_text

def lambda_handler(event, context):
    try:        
        if 1:
            #S3_Polly
            session = boto3.Session()
            polly = boto3.client("polly")
        
            s3 = session.resource('s3')
            bucket = s3.Bucket("polly.speech")
                        
            # Parse the event body as a JSON object
            #if json.loads(event)["events"][0]["message"]["type"] == "text":# textの場合のみ対応
            if event["events"][0]["message"]["type"] == "text":# textの場合のみ対応
                reply_token = event["events"][0]['replyToken']# リプライ用トークン
                prompt_text = event["events"][0]["message"]["text"]# 受信メッセージ
                line_user_id  = event["events"][0]['source']['userId']# userID

                # Check if the event is a message type and is of text type
                if prompt_text is None or line_user_id is None or reply_token is None:
                    raise Exception('Elements of the event body are not found.')

                # Create the completed text by Chat-GPT 3.5 turbo
                completed_text, history_text, conversation_num = create_completed_text(line_user_id, prompt_text)
                
                print("completed_text", completed_text)

                #Polly output
                if 1:
                    response = polly.synthesize_speech(
                        Text=completed_text,
                        #Engine="neural",
                        OutputFormat="mp3",
                        VoiceId="Mizuki")
                    with closing(response["AudioStream"]) as stream:
                        bucket.put_object(Key="<任意のファイル名>.mp3", Body=stream.read(), ACL="public-read")
                    
                #バケット内の音声
                original_content_url=<ここにS3に保管したmp3ファイルのアクセスパスを入れる>
                
                # Reply the message using the LineBotApi instance
                reply_message_for_line(reply_token, completed_text, original_content_url)

    except Exception as e:
        # Log the error
        
        # Return 200 even when an error occurs as mentioned in Line API documentation
        return {'statusCode': 200, 'body': json.dumps(f'Exception occurred: {e}')}
    
    # Return a success message if the reply was sent successfully
    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}


Amazon Pollyに渡す引数設定や、S3からファイルを取り出すためのコードは、AWSのドキュメントが一番詳しく書いてあるが、簡単に以下説明を追加した。

#Polly output
response = polly.synthesize_speech(
Text=completed_text,
OutputFormat="mp3",#出力ファイル形式設定
VoiceId="Mizuki") #出力ボイスの選択
with closing(response["AudioStream"]) as stream:
bucket.put_object(Key="<任意のファイル名>.mp3", Body=stream.read(), ACL="public-read") #キーとファイルの組み合わせで格納する。ACL="public-read"とすることで、更新したファイルのパブリックアクセスできるようになる
                    
#バケット内の音声
original_content_url=<ここにS3に保管したmp3ファイルのアクセスパスを入れる>#保存したファイルのアクセスパス


実行結果

ブログなので、結果を表現しずらいですが、、あいさつに対して、ChatGPTが適切な挨拶を考えて、その文言が音声として帰ってきます。

実行結果


まとめ

今回は、複数のAWSサービスとLINEを連携させ、LINEに音声で返すところまでを構築しました。
これをベースにして、任意のキャラクターとLINEで会話が楽しめるようなシステムを作っていこうと思います。

Python(BeautifulSoup4):ネットのページから必要情報を取得する(野球の試合状況を取得する)

特定の(最新)情報を入手して、何かをしたい場合、情報入手先にアクセスして、情報を取得する作業が必要となります。その方法をこのページではまとめます。
今回はその例として野球試合情報を入手するコードを紹介します。

目次

使用環境

$wmic os get caption
>Microsoft Windows 10 Pro

$wmic os get osarchitecture
>64ビット

$python -V
Python 3.11.2

手順

  1. 必要モジュールのインストール
  2. コード作成/実行

必要モジュールのインストール

今回は、Requests, BeautifulSoup4モジュールを使用します。
Requestsモジュールは、HTMLデータの取得に使用します。
その後、必要情報のみを抽出する為、HTMLデータの抽出で使用するのが、BeautifulSoup4モジュールになります。
HTMLとは「ハイパーテキストマークアップ・ランゲージ(Hyper Text Markup Language)」のことで、WEBページを作成するための言語で、ほとんどのウェブページは、HTMLで作成されています。
Google Chrome(グーグルクローム)のブラウザを使う場合、F12キーを押すと、デベロッパーツールが起動して、HTMLの内容を確認できますね。必要情報が、HTML内のどこに存在するのかを確かめながら、コードを組むのがよさそうです。

まずはBeautiful Soupをインストールしましょう。pipのコマンドでインストールすることが出来ます。
モジュールの詳細は以下をご参照ください。
www.crummy.com

pip install requests
pip install beautifulsoup4


コード作成/実行

今回の目的(野球の試合状況を取得する)は、以下コードで実現しています。
このコードを短期間で連続使用する場合、サーバーに負荷がかかるため、気を付けた方がよさそうです。

import requests
from datetime import datetime
from time import sleep
from bs4 import BeautifulSoup

#######野球設定
OUR_TEAM = "広島" #情報取得したいチーム名をTEM_LIST内から1チーム選ぶ
TEAM_LIST = ["広島", "ヤクルト", "阪神", "DeNA", "中日", "巨人"]

def live_scores(date) ->"yyyy-mm-dd":

    # URLを指定
    url = "https://baseball.yahoo.co.jp/npb/schedule/?date=" + date

    # URLにアクセスしてHTMLを取得
    try:
        print(url)
        response = requests.get(url)
    except: 
        print('Error')
    html = response.text

    # HTMLを解析してBeautiful Soupオブジェクトを作成
    soup = BeautifulSoup(html, "html.parser")
    #print("soup",soup)

    # スコアのテーブルを取得    
    game_card = soup.find_all("ul", class_="bb-score__card bb-score__card--separate")
    game_card_several_team_name = soup.find_all("div", class_="bb-score__team")
    game_card_several_score = soup.find_all("div", class_="bb-score__detail")

    num = []
    for row in range(len(game_card_several_team_name)):
        # 日付、対戦相手、スコア、勝敗、詳細ページのURLを取得
        if OUR_TEAM in str(game_card_several_team_name[row]):
            num.append(row)

    if num :
        check_dict = {}
        buff = game_card_several_team_name[num[0]].find_all("p")
        for i in range(len(buff)):
            for name in TEAM_LIST:
                if name in str(buff[i]):
                    check_dict[name] = i
        n=0
        for i in list(check_dict.keys()):
            if n == 0:
                home_team = i
            else:
                away_team = i
            n+=1
        if home_team == OUR_TEAM:
            home_team_flg = 1
        else:
            home_team_flg = 0
        
        print(home_team, away_team)

        try: 
            if home_team_flg:
                game_status = "ホーム"
                home_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--left").text.strip()
                away_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--right").text.strip()
            else:
                game_status = "アウェイ"
                away_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--left").text.strip()
                home_score = game_card_several_score[num[0]].find("span", class_="bb-score__score bb-score__score--right").text.strip()
                
            now_game_status = game_card_several_score[num[0]].find("p", class_="bb-score__link").text.strip()        
            if now_game_status == "試合終了":
                if home_score > away_score:
                    status_comment = "勝った"
                elif home_score < away_score:
                    status_comment = "負けた"
                elif home_score == away_score:
                    status_comment = "引き分けた"
            else:
                if home_score > away_score:
                    status_comment = "勝っている"
                elif home_score < away_score:
                    status_comment = "負けている"
                elif home_score == away_score:
                    status_comment = "引き分けている"
            return date + "の" + OUR_TEAM + "の試合は" + now_game_status + "で、スコアは" + home_score + "-" + away_score + "で"+status_comment
        except:
            print("skip")
        
            now_game_status = game_card_several_score[num[0]].find("p", class_="bb-score__link").text.strip()
            return  date + "は" + OUR_TEAM + "の試合は開催前で、午後開催" 
    else:
        return  OUR_TEAM + "は" + "の試合はない"


簡単にコードを解説すると、

  • requests.get(url)にて指定のURLのHTMLを取得する
  • soup = BeautifulSoup(html, "html.parser")にてHTMLを解析する(HTMLパーサー)

 Webページから必要情報を取得したりすることを、スクレイピングと呼びますが、スクレイピングは、“データを収集した上で利用しやすく加工すること"のようです。その後、必要情報を取得するコードは、BeautifulSoupライブラリを使って実行しており、今回は、".find"などを使って、必要情報を抽出しています


参考までに実行結果を紹介すると、

live_scores("2023-04-22")

Output: '2023-04-22の広島の試合は試合終了で、スコアは3-0で勝った'


まとめ

今回は、ネットページから必要情報を取得する例を紹介しました。
定期的に何かの情報を取得して使用するといった処理が必要な場合に重宝しそうですね