見出し画像

誕生日お祝いアプリのバージョンアップ〜OpenAI API × Streamlit:環境構築・エラー対応・デプロイまで〜

誕生日お祝いアプリのバージョンアップ記録

〜OpenAI API × Streamlit:環境構築・エラー対応・デプロイまで〜

この記事は、アプリ作成初心者の私が OpenAI API + Streamlit を使って「誕生日お祝いアプリ」を作り、
ローカル開発からクラウド公開まで完走する中で経験した 実際のエラーと学び をまとめたものです。
前回の記事「cursor による自動コーディング」の続きです。



📌 この開発記録で分かること

  • 初心者(私も)が どんなエラーにつまずくか

  • GPT‑5 系モデル特有のポイント

    • temperature 廃止

    • reasoning トークンの存在

  • Streamlit Cloud で 画面が真っ白になる原因と対処法

  • agent(AI コーディング補助)が できること・できないこと


■ 1. 仮想環境の準備とアプリ起動(最初の一歩)

まずは .env に API キーを設定し、Python の仮想環境を構築して依存パッケージをインストールします。

python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

久しぶりの作業だったため、コマンドを思い出すのにもひと苦労でした。
ChatGPT に確認しながら進めるという、ごく普通の開発者のスタートです。

そして実行。

アプリが立ち上がった!背景が黒でカッコいい。

誕生日の入力欄がカレンダー UI で表示され、テンションが上がりました。


■ 2. エラー①:messages パラメータエラー

〜agent (AI コーディング補助)が修正〜

● 出たエラー

Responses.create() got an unexpected keyword argument 'messages'

● 原因

Responses API の仕様に対し、パラメータ名が間違っていた

● どう直したか

cursor の agent に修正依頼。

ついでに agent に新しいモデルを指定して変更してとお願いしました。
モデルを gpt‑4o → gpt‑5-mini に変更してくれました。


■ 3. エラー②:temperature が使えない

〜GPT‑5 で仕様が変わっていた〜

● 出たエラー

Unsupported parameter: 'temperature' is not supported with this model.

● 原因

GPT‑4o 用コードの名残で temperature が残っていた。
実は GPT‑5 系では temperature が固定化され、指定できない 仕様になっていたのです。

これは、agent との対話では分からなくて、自分で調べて判明しました。
cursor のドキュメント参照をうまく使えば、もっと早く判明したかもしれません。

● どう直したか

👉 temperature を削除

● 小さな学び

LLM のバージョンアップは、意外な仕様変更に気づきにくい。
エラーメッセージは “仕様変更に気づくヒント” になる。


■ 4. エラー③:レスポンスは返るのに、内容がおかしい

〜agent でも直らず、モデル変更で突破〜

● 状況

temperature を消すとレスポンスは返ってくるようになりました。

しかし、レスポンスは返ってくるのに、表示される内容が正しく生成文ではなく、モデル内部の推論情報やエラー文のような内容になっていました。

agent にエラーを伝えて 2 回修正依頼しても 改善せず

● どう対処したか

試しにモデルを変更してみました。

👉 auto → sonnet に変更

すると、gpt-5は推論を使っているのでトークンオーバーしていることが判明し、トークン制限をアップ(500→2000)しました。
同時に、agent は開いていた他のプロジェクトのコードを参考にして、レスポンスからのテキスト抽出処理が冗長だったことを発見し、適切に修正してくれました。気が利いています。

● 学んだこと:reasoning トークンの存在

  • gpt‑5-mini は 推論プロセス(reasoning) を内部で生成する

  • これが 追加のトークン として消費される

  • その結果、max_output_tokens に達して status='incomplete' となっていた

reasoning のイメージ

モデルが “頭の中で考えているメモ書き” のようなもの。
それもトークンとしてカウントされるため、出力が途中で止まることがある。


■ 5. ローカルでついに動作! 🎉

〜動いた瞬間は本当にうれしい〜

誕生日を入れると、画面いっぱいにアニメーションが広がり、お祝いメッセージが生成される。

「これは嬉しい!」

ローカルでここまで動かすのに、体感でも 1時間くらいエラーと格闘していました


■ 6. エラー④:Streamlit Cloud で画面が真っ白

〜原因は “inotify の監視上限” だった〜

ローカルで動いたので、次は Streamlit Cloud へデプロイ。

  • GitHub リポジトリ登録

  • 環境変数に API キー設定

  • デプロイ開始

…しかし画面が 真っ白

● ヒントは右下の黒いボタン「Manage app」

そこに インストールログ・エラーログ があり、ダウンロード可能でした。

ログを ChatGPT に渡すと、原因が判明。

● 原因(やさしくまとめると)

  • Streamlit は変更を監視する

  • 監視には Linux の inotify を使う

  • Cloud では「監視できる数」に上限がある

  • この上限に達すると、追加の監視ができずエラー

  • → inotify instance limit reached

● どう直したか

.streamlit/config.toml に以下を追加:

[server]
fileWatcherType = "none"

これで「監視しなくていいよ」と Streamlit に伝えられる。

agent にファイル生成を依頼し、再デプロイすると……
ついにクラウドでも動作!

ローカル → クラウド の突破は達成感が大きい瞬間でした。


■ 7. 今後の予定(Next Steps)

  • コード全体をじっくり読み、Google Colab で動作確認(次回)

  • 誕生日メッセージのバリエーション強化 (将来)

読んでくださった方も、まずは 小さなアプリを動かすところから 始めてみてはいかがでしょうか。
例えば、「今日の天気を教えてくれるアプリ」や「メッセージを一言生成するだけのアプリ」でも十分だと思います。


■ 8. 公開アプリはこちら

👉 Birthday App


アプリが停止していたら、青いボタンを押して、起動までしばらく待ってください。

■ 9. おわりに 〜エラーは “敵” ではなく “教材” 〜

今回のアプリ開発では、

  • GPT モデル仕様の変化

  • reasoning トークンの存在

  • エラーログの読み解き

  • agent でも修正できないケースがあること

  • クラウド固有の落とし穴(inotify)

といった貴重な学びがありました。

AI がコードを書いても、
新しい API や仕様変更まわりは、まだ誤りやすい領域。

自動コーディングに任せることで、
一つひとつ動作確認をしながら組み立てていく「手を動かして作る楽しさ」は、少し減ったようにも感じました。
その一方で、「こういうアプリを作りたい」と思ったときに、前回の記事でも触れましたが、完成形に一気に近づけるスピード感と、短時間で形になること自体の楽しさと感動も、大きく感じました。

だからこそ、人間がログを読み、原因を推測し、
“LLM に指示を出す力” が大事だと実感しました。

エラーは、避けるべき敵ではなく、
理解を深めてくれる最高の教材 です。

この記録が、あなたの「ちょっとやってみようかな」の背中を押せたら嬉しいです。


自己紹介(私のページ)


アプリコードの参考:githubのリンクとコード


import streamlit as st
from datetime import datetime, date
import pytz
import os
from openai import OpenAI
from dotenv import load_dotenv

# 環境変数の読み込み
load_dotenv()

# OpenAI クライアントの初期化
client = None
try:
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        # Streamlit Cloud の Secrets Manager を確認
        if hasattr(st, "secrets") and "OPENAI_API_KEY" in st.secrets:
            api_key = st.secrets["OPENAI_API_KEY"]

    if api_key:
        client = OpenAI(api_key=api_key)
except Exception as e:
    st.error(f"OpenAI API の初期化に失敗しました: {str(e)}")


def get_today_jst():
    """日本時間(JST)で今日の日付を取得"""
    jst = pytz.timezone("Asia/Tokyo")
    return datetime.now(jst).date()


def calculate_days_lived(birthday: date) -> int:
    """誕生日から今日までの日数を計算"""
    today = get_today_jst()
    days = (today - birthday).days
    return days


def calculate_age(birthday: date) -> int:
    """現在の年齢を計算"""
    today = get_today_jst()
    age = today.year - birthday.year
    # 誕生日がまだ来ていない場合は1歳引く
    if (today.month, today.day) < (birthday.month, birthday.day):
        age -= 1
    return age


def is_birthday_today(birthday: date) -> bool:
    """今日が誕生日かどうかを判定"""
    today = get_today_jst()
    return (today.month, today.day) == (birthday.month, birthday.day)


def generate_birthday_message(
    days_lived: int, age: int = None, is_birthday: bool = False
) -> str:
    """OpenAI Responses API を使ってお祝いメッセージを生成"""
    if not client:
        return "⚠️ OpenAI API が利用できません。API キーを設定してください。"

    try:
        # プロンプトの作成
        if is_birthday:
            prompt = f"""
あなたは優しく、温かい心を持ったメッセージ作成者です。
今日は誕生日を迎えた人への特別なお祝いメッセージを作成してください。

情報:
- 今日で{age}歳になりました
- 生まれてから{days_lived}日が経ちました

以下の要件でメッセージを作成してください:
1. 誕生日おめでとうの気持ちを込める
2. 優しく、ほのぼのとしたトーン
3. 「生きててよかった」「自分の人生も悪くない」と思えるような内容
4. {days_lived}日という日数の重みを感じさせる
5. 200文字以内
6. 絵文字を適度に使用(🎂🎉✨など)
"""
        else:
            prompt = f"""
あなたは優しく、温かい心を持ったメッセージ作成者です。
生まれてから{days_lived}日生きてきた人へのお祝いメッセージを作成してください。

以下の要件でメッセージを作成してください:
1. 優しく、ほのぼのとしたトーン
2. 「生きててよかった」「自分の人生も悪くない」と思えるような内容
3. {days_lived}日という日数の重みを感じさせる
4. 前向きで温かい気持ちになれる
5. 200文字以内
6. 絵文字を適度に使用(✨🌸💖など)
7. 毎回違う表現を使い、似た表現の連続を避ける
"""

        # システムプロンプトとユーザープロンプトを結合
        system_prompt = (
            "あなたは優しく、人を励ますことが得意なメッセージ作成の専門家です。"
        )
        full_prompt = f"{system_prompt}\n\n{prompt}"

        # OpenAI Responses API 呼び出し
        response = client.responses.create(
            model="gpt-5-mini",  # Responses API推奨モデル
            input=full_prompt,  # inputパラメータを使用
            max_output_tokens=2000,  # reasoning用に余裕を持たせる
        )

        # Responses APIの応答を取得
        if hasattr(response, "output_text") and response.output_text:
            return response.output_text.strip()
        else:
            return f"⚠️ APIから正常なレスポンスが得られませんでした。status: {getattr(response, 'status', 'unknown')}"

    except Exception as e:
        return f"⚠️ メッセージの生成中にエラーが発生しました: {str(e)}"


def main():
    """メインアプリケーション"""

    # ページ設定
    st.set_page_config(
        page_title="生まれてきてくれ、ありがとう", page_icon="🎂", layout="centered"
    )

    # タイトル
    st.title("🎂 生まれてきてくれ、ありがとう")
    st.markdown("---")

    # 説明文
    st.markdown("""
    あなたが生まれてから今日まで、どれだけの日々を過ごしてきたか知っていますか?
    
    誕生日を入力すると、**生きてきた日数**を計算し、
    あなたへの特別なお祝いメッセージをお届けします ✨
    """)

    st.markdown("---")

    # 誕生日入力
    st.subheader("📅 あなたの誕生日を教えてください")

    # 日付入力フィールド
    birthday = st.date_input(
        "誕生日を選択してください",
        value=None,
        min_value=date(1900, 1, 1),
        max_value=get_today_jst(),
        format="YYYY/MM/DD",
    )

    # ボタンと処理
    if st.button("🎉 お祝いメッセージを見る", type="primary", use_container_width=True):
        if birthday is None:
            st.warning("⚠️ 誕生日を入力してください")
        else:
            # 未来の日付チェック
            if birthday > get_today_jst():
                st.error("⚠️ 未来の日付は入力できません")
            else:
                # 日数計算
                days_lived = calculate_days_lived(birthday)

                # 誕生日当日かチェック
                is_birthday = is_birthday_today(birthday)
                age = calculate_age(birthday) if is_birthday else None

                # 結果表示
                st.markdown("---")

                if is_birthday:
                    st.success("### 🎊 お誕生日おめでとうございます! 🎊")
                    st.balloons()  # 風船演出
                    st.metric(
                        label="今日で", value=f"{age}歳", delta="Happy Birthday! 🎂"
                    )

                st.metric(
                    label="あなたが生きてきた日数",
                    value=f"{days_lived:,}日",
                    delta="毎日がかけがえのない一日 ✨",
                )

                # メッセージ生成
                with st.spinner("心を込めてメッセージを作成しています..."):
                    message = generate_birthday_message(days_lived, age, is_birthday)

                # メッセージ表示
                st.markdown("### 💌 あなたへのメッセージ")
                st.info(message)

                st.markdown("---")
                st.markdown(
                    """
                <div style='text-align: center; color: #888; font-size: 0.9em;'>
                    生きてくれて、ありがとう。<br>
                    あなたの存在が、誰かの幸せになっています。
                </div>
                """,
                    unsafe_allow_html=True,
                )

    # フッター
    st.markdown("---")
    st.markdown(
        """
    <div style='text-align: center; color: #888; font-size: 0.8em; margin-top: 2em;'>
        💝 このアプリは、あなたの人生を祝うために作られました 💝
    </div>
    """,
        unsafe_allow_html=True,
    )


if __name__ == "__main__":
    main()

いいなと思ったら応援しよう!