Gemini API キー,LangChain による回答添削,画像理解(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install langchain-google-genai pillow SpeechRecognition pyaudio opencv-python

Gemini API キー,LangChain による回答添削,画像理解プログラム

概要

このプログラムは、Google Gemini 2.5 FlashのAIを使って画像を解析するGUIアプリケーションである。画像ファイルの選択,カメラで撮影の機能がある.質問を入力(音声入力も可能)して「画像を解析」ボタンを押すと、AIが画像の内容を説明する。APIキーは.envファイルから自動読み込み可能であり,画面から入れることもできる.解析結果は画面表示と同時にresult.txtファイルに自動保存される。

ソースコード


"""
プログラム名: Gemini API 画像解析ツール
特徴技術名: Google Gemini API (Gemini 2.5 Flash)
出典: Google AI Studio Documentation (2024). Gemini API. https://ai.google.dev/docs
特徴機能: マルチモーダル画像理解機能 - 画像とテキストを同時に理解し、画像の内容について詳細な説明、分析、質問への回答を生成
学習済みモデル: Gemini 2.5 Flash (クラウドAPI経由で使用、ダウンロード不要)
方式設計:
  - 関連利用技術:
    - LangChain (LLMとの統合フレームワーク、画像とテキストの構造化されたメッセージ送信)
    - OpenCV (Webカメラからの画像キャプチャ)
    - Tkinter (GUIフレームワーク)
    - SpeechRecognition (Google Speech Recognition APIによる音声入力)
    - PIL/Pillow (画像のリサイズとプレビュー表示)
  - 入力と出力: 入力: 静止画像(GUIの「画像ファイルを選択」ボタンでファイル選択、「カメラで撮影」ボタンでOpenCVカメラ起動しスペースキーで撮影)、出力: テキスト形式の解析結果(GUI画面に表示、result.txtファイルに保存)
  - 処理手順: 1.画像をBase64エンコード、2.LangChainのHumanMessageに画像とプロンプトを構造化、3.Gemini APIに送信、4.マルチモーダル理解による解析結果を取得、5.結果をGUIに表示とファイル保存
  - 前処理、後処理: 前処理: 画像のBase64エンコード、MIMEタイプの自動判定。後処理: 解析結果のタイムスタンプ付き履歴保存
  - 追加処理: プロンプトテンプレート機能(学習支援、シーン分析、場所別分析)により、特定用途に最適化された解析を実現
  - 調整を必要とする設定値: temperature (現在0に設定、創造性と正確性のバランスを制御、0-2の範囲で調整可能)
将来方策: temperatureの自動調整機能 - 解析対象(学習問題、シーン、場所)に応じてtemperatureを動的に変更し、最適な解析結果を得る
その他の重要事項: APIキーは.envファイルまたはGUI入力で設定。無料枠の制限あり。画像サイズは自動的に最適化
前準備:
  - pip install opencv-python pillow langchain-google-genai SpeechRecognition
"""

import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext
import base64
import os
import cv2
from PIL import Image, ImageTk
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
import datetime
import threading
import speech_recognition as sr

# 設定定数
# ウィンドウ設定
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 700
PREVIEW_MAX_WIDTH = 250
PREVIEW_MAX_HEIGHT = 200
QUESTION_HEIGHT = 8
QUESTION_WIDTH = 40
RESULT_HEIGHT = 15
RESULT_WIDTH = 80
API_ENTRY_WIDTH = 50

# ファイル設定
TEMP_IMAGE_FILE = 'temp_captured_image.jpg'
RESULT_FILE = 'result.txt'
ENV_FILE = '.env'

# 色設定(ボタンの背景色)
COLOR_PRIMARY = '#007bff'      # 青(学習支援)
COLOR_SECONDARY = '#6c757d'    # グレー(自由記入、使用方法)
COLOR_SUCCESS = '#28a745'      # 緑(画像選択)
COLOR_DANGER = '#dc3545'       # 赤(画像解析)
COLOR_WARNING = '#ffc107'      # 黄(音声入力)
COLOR_INFO = '#17a2b8'         # 水色(カメラ)
COLOR_PURPLE = '#9333ea'       # 紫(シーン分析)
COLOR_PINK = '#f43f5e'         # ピンク(場所別分析)

# ボタンの押下時の色
COLOR_PRIMARY_DARK = '#0056b3'
COLOR_SECONDARY_DARK = '#495057'
COLOR_PURPLE_DARK = '#a855f7'
COLOR_PINK_DARK = '#e11d48'

# API設定
TEMPERATURE = 0  # Gemini APIの温度パラメータ(0-2の範囲、低いほど確定的)
MODEL_NAME = 'gemini-2.5-flash'

# グローバル変数の初期化
api_key = ''
llm = None
img_path = ''
root = None
api_key_var = None
img_path_var = None
img_path_label = None
image_label = None
q_text = None
r_text = None
v_button = None
v_status_var = None
progress = None
recognizer = None
microphone = None
hint_text = None
learning_button = None
free_button = None
scene_button = None
location_button = None

# .envファイルからAPIキー読み込み
if os.path.exists(ENV_FILE):
    try:
        with open(ENV_FILE, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    if line.startswith('GEMINI_API_KEY='):
                        api_key = line.split('=', 1)[1].strip()
    except Exception as e:
        print(f'.envファイルの読み込みに失敗しました: {str(e)}')

# 音声認識の初期化
try:
    recognizer = sr.Recognizer()
    microphone = sr.Microphone()
except Exception as e:
    print(f'音声認識の初期化に失敗しました: {str(e)}')
    recognizer = None
    microphone = None

# プロンプト定義
prompt_learn = '''この画像は学習問題とその手書き解答を含んでいます。以下の観点で詳細に分析してください:

1. **解答の正確性**: 正解・不正解の判定と根拠
2. **解答プロセス**: 思考過程や解法の適切性
3. **表現・記述**: 字の読みやすさ、図表の工夫など
4. **評価とフィードバック**:
   - 良い点(具体的に褒める部分)
   - 改善点(1つの重要なアドバイス)
   - 理解度の推定
5. **教育的補足**: 問題に関する解説や関連知識

学習者のモチベーション向上を重視し、建設的で温かい添削をお願いします。'''

prompt_scene = '''この画像を詳細に分析し、以下の観点で情報を提供してください:

1. **場所・シーンの特定**: 画像に写っている場所やシーンの種類
2. **重要な情報**: 標識、表示、文字情報など
3. **安全性の確認**: 危険箇所、注意すべき点
4. **状態の評価**: 設備や環境の状態
5. **その他の観察事項**: 特筆すべき点や気づいたこと

実用的で具体的な情報を提供してください。'''

prompt_location = '''この画像について、場所の種類(駅、空港、ショッピングモール、その他)を特定し、以下の観点で詳細に分析してください:

1. **場所の特定と基本情報**:
   - 場所の種類(屋内/屋外、建物の種類、具体的な場所名)
   - プラットフォーム番号、ゲート番号、店舗名など場所固有の識別情報
   - 営業時間、運行情報などの時間に関する情報

2. **案内・方向情報**:
   - 出口案内、階段、エスカレーター、エレベーターの位置
   - フロアガイド、施設配置図
   - 行き先表示、方向指示

3. **施設・サービス情報**:
   - トイレ、売店、レストラン、休憩所などの位置
   - 特別なサービス(ラウンジ、免税店、コインロッカーなど)
   - バリアフリー設備

4. **重要な表示・告知**:
   - 時刻表、フライト情報、セール情報
   - 注意事項、警告表示
   - イベント情報、臨時案内

5. **文字情報の翻訳と解説**:
   - 看板、標識、標示に書かれている文字情報(英語表記は日本語に翻訳)
   - 時間帯、天候、混雑状況などの状況
   - その他の特徴的で有用な情報

必ず日本語で回答し、実用的な情報を提供してください。'''

# ヒントテキスト定義
hint_learn = '''【学習問題の添削機能】
本モードは学習問題と手書き解答の添削に最適化されています:
・解答の正確性を判定
・思考過程を分析
・改善点をアドバイス
・学習者のモチベーション向上を重視

【画像の準備】
・問題文と手書き解答が両方写っている画像を使用
・文字が鮮明で読み取りやすいことを確認
・影や反射がない、均一な照明で撮影

【効果的な使い方】
・1つの問題ずつ撮影して個別に添削
・複数ページある場合は各ページを分けて撮影
・図形問題では図と計算過程の両方を含める
・文章題では考え方のメモも写す

【添削結果の活用】
・良い点を確認して自信を持つ
・改善アドバイスを次回に活かす
・解説を読んで理解を深める
・結果を保存して復習に使う'''

hint_free = '枠に自由に記入できます'

hint_scene = '''【シーン分析の撮影のコツ】
・画像全体が明瞭に写るように撮影
・標識や表示がある場合は読み取れるように
・周囲の環境も含めて撮影
・複数の角度から撮影すると効果的
・照明条件に注意(逆光を避ける)

【活用例】
・現場の記録と確認
・設備の状態チェック
・安全確認の補助
・環境調査の記録'''

hint_location = '''【場所別分析の撮影のコツ】
・案内板や標識を含めて撮影
・場所の全体像が分かるように
・文字情報が読み取れる解像度で
・複数の案内表示がある場合は個別に撮影
・方向や位置関係が分かるように

【効果的な撮影対象】
・駅の時刻表や路線図
・空港の出発/到着案内板
・ショッピングモールのフロアガイド
・観光地の案内図
・施設の利用案内'''

# ヒント更新関数
def update_hint(text):
    """ヒントテキストを更新"""
    hint_text.config(state=tk.NORMAL)
    hint_text.delete('1.0', tk.END)
    hint_text.insert('1.0', text)
    hint_text.config(state=tk.DISABLED)

# プロンプト設定関数
def set_prompt(prompt_text, active_btn, active_color, hint):
    """プロンプトとボタン状態を設定"""
    q_text.delete('1.0', tk.END)
    q_text.insert('1.0', prompt_text)

    # ボタンの状態をリセット
    buttons = [
        (learning_button, COLOR_PRIMARY),
        (free_button, COLOR_SECONDARY),
        (scene_button, COLOR_PURPLE),
        (location_button, COLOR_PINK)
    ]

    for btn, color in buttons:
        btn.config(relief=tk.RAISED, bg=color)

    # アクティブボタンの設定
    active_btn.config(relief=tk.SUNKEN, bg=active_color)
    update_hint(hint)

# APIキー設定処理
def set_api_key():
    """APIキーを設定し、LLMインスタンスを初期化"""
    global api_key, llm
    api_key = api_key_var.get().strip()
    if not api_key:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', 'APIキーが入力されていません。')
        return

    try:
        os.environ['GOOGLE_API_KEY'] = api_key
        llm = ChatGoogleGenerativeAI(
            model=MODEL_NAME,
            temperature=TEMPERATURE
        )
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', 'APIキーが設定されました。')
    except Exception as e:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', f'APIキーの設定に失敗しました: {str(e)}')

# カメラ撮影処理
def capture_from_camera():
    """Webカメラから画像を撮影"""
    global img_path

    try:
        cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
        if not cap.isOpened():
            r_text.delete('1.0', tk.END)
            r_text.insert('1.0', 'カメラを開けませんでした。')
            return

        # バッファサイズを設定
        cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

        while True:
            cap.grab()
            ret, frame = cap.retrieve()
            if not ret:
                break

            cv2.imshow('Camera - Press SPACE to capture, Q to quit', frame)
            key = cv2.waitKey(1) & 0xFF

            if key == ord(' '):
                cv2.imwrite(TEMP_IMAGE_FILE, frame)
                img_path = TEMP_IMAGE_FILE
                img_path_var.set('撮影した画像')

                # 画像プレビュー表示
                image = Image.open(TEMP_IMAGE_FILE)
                image.thumbnail((PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.Resampling.LANCZOS)
                photo = ImageTk.PhotoImage(image)
                image_label.configure(image=photo, text='')
                image_label.image = photo

                break
            elif key == ord('q'):
                break

        cap.release()
        cv2.destroyAllWindows()

    except Exception as e:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', f'カメラ撮影エラー: {str(e)}')

# 画像ファイル選択処理
def select_image():
    """画像ファイルを選択"""
    global img_path
    filetypes = [
        ('画像ファイル', '*.jpg *.jpeg *.png *.gif *.bmp *.tiff *.webp'),
        ('JPEG files', '*.jpg *.jpeg'),
        ('PNG files', '*.png'),
        ('GIF files', '*.gif'),
        ('BMP files', '*.bmp'),
        ('TIFF files', '*.tiff *.tif'),
        ('WebP files', '*.webp'),
        ('すべてのファイル', '*.*')
    ]

    filename = filedialog.askopenfilename(
        title='画像ファイルを選択',
        filetypes=filetypes,
        initialdir=os.getcwd()
    )

    if filename:
        img_path = filename
        img_path_var.set(os.path.basename(filename))

        try:
            # 画像プレビュー表示
            image = Image.open(filename)
            image.thumbnail((PREVIEW_MAX_WIDTH, PREVIEW_MAX_HEIGHT), Image.Resampling.LANCZOS)
            photo = ImageTk.PhotoImage(image)
            image_label.configure(image=photo, text='')
            image_label.image = photo
        except Exception as e:
            r_text.delete('1.0', tk.END)
            r_text.insert('1.0', f'画像の読み込みエラー: {str(e)}')

# プロンプト設定関数(個別)
def set_learning_support():
    """学習支援プロンプトを設定"""
    set_prompt(prompt_learn, learning_button, COLOR_PRIMARY_DARK, hint_learn)

def set_free_input():
    """自由入力モードを設定"""
    set_prompt('', free_button, COLOR_SECONDARY_DARK, hint_free)

def set_scene_analysis():
    """シーン分析プロンプトを設定"""
    set_prompt(prompt_scene, scene_button, COLOR_PURPLE_DARK, hint_scene)

def set_location_analysis():
    """場所別分析プロンプトを設定"""
    set_prompt(prompt_location, location_button, COLOR_PINK_DARK, hint_location)

# 音声入力スレッド処理
def voice_input_thread():
    """音声入力を処理するスレッド"""
    try:
        root.after(0, lambda: v_status_var.set('音声を聞いています...'))
        root.after(0, lambda: v_button.config(state='disabled'))

        with microphone as source:
            recognizer.adjust_for_ambient_noise(source, duration=1)
            audio = recognizer.listen(source, timeout=10)

        root.after(0, lambda: v_status_var.set('音声を認識中...'))
        text = recognizer.recognize_google(audio, language='ja-JP')
        root.after(0, lambda: q_text.delete('1.0', tk.END))
        root.after(0, lambda: q_text.insert('1.0', text))
        root.after(0, lambda: v_status_var.set('認識完了'))

    except sr.UnknownValueError:
        root.after(0, lambda: v_status_var.set('音声を認識できませんでした'))
    except sr.RequestError as e:
        root.after(0, lambda: v_status_var.set(f'音声認識エラー: {str(e)}'))
    except sr.WaitTimeoutError:
        root.after(0, lambda: v_status_var.set('タイムアウト:音声が検出されませんでした'))
    except Exception as e:
        root.after(0, lambda: v_status_var.set(f'エラー: {str(e)}'))
    finally:
        root.after(0, lambda: v_button.config(state='normal'))
        root.after(3000, lambda: v_status_var.set(''))

# 音声入力開始
def voice_input():
    """音声入力を開始"""
    if not recognizer or not microphone:
        v_status_var.set('音声認識が利用できません')
        root.after(3000, lambda: v_status_var.set(''))
        return

    thread = threading.Thread(target=voice_input_thread)
    thread.daemon = True
    thread.start()

# 使用方法ガイド表示
def show_usage_guide():
    """使用方法ガイドを別ウィンドウで表示"""
    guide_window = tk.Toplevel(root)
    guide_window.title('使用方法ガイド')
    guide_window.geometry('700x600')

    guide_text = '''
Gemini API 画像解析ツール 使用ガイド

【初期設定】
1. Google AI StudioでAPIキーを取得
   - https://aistudio.google.com/app/apikey にアクセス
   - Googleアカウントでログイン
   - 「Get API key」→「Create API key」をクリック
   - 生成されたAPIキーをコピー

2. APIキーの設定
   - 本アプリケーションのAPIキー入力欄に貼り付け
   - 「APIキー設定」ボタンをクリック
   - または、.envファイルに「GEMINI_API_KEY=YOUR_API_KEY」として保存

【画像の準備】
◆ ファイル選択
   - 「画像ファイルを選択」ボタンをクリック
   - 解析したい画像を選択
   - 対応形式: JPG, PNG, GIF, BMP, TIFF, WebP
   - 選択後、プレビューが表示されます

◆ カメラ撮影
   - 「カメラで撮影」ボタンをクリック
   - カメラウィンドウが開きます
   - スペースキー: 撮影
   - Qキー: キャンセル

【質問・指示の入力】
◆ テキスト入力
   - 質問欄に直接入力
   - デフォルトは学習問題の添削用プロンプト

◆ 音声入力
   - 「音声入力」ボタンをクリック
   - マイクに向かって話す
   - 自動的にテキスト変換されます

【解析実行】
1. 画像が選択されていることを確認
2. 質問・指示が入力されていることを確認
3. 「画像を解析」ボタンをクリック
4. 解析結果が下部に表示されます

【解析結果の保存】
- 解析結果は自動的にresult.txtファイルに保存
- タイムスタンプ付きで履歴管理
- 後から振り返ることが可能

【トラブルシューティング】
◆ APIキーエラー
   - APIキーが正しく入力されているか確認
   - インターネット接続を確認

◆ カメラが起動しない
   - 他のアプリケーションがカメラを使用していないか確認
   - カメラのアクセス許可を確認

◆ 音声入力が動作しない
   - マイクの接続を確認
   - マイクのアクセス許可を確認
   - 静かな環境で再試行

【注意事項】
- このツールは学習サポートが目的です
- 最終的な理解は自分で確認しましょう
- 個人情報(名前等)が写らないよう注意
- API利用制限に注意(無料枠あり)
- 大きすぎる画像は自動的にリサイズされます
    '''

    text_widget = scrolledtext.ScrolledText(guide_window, wrap=tk.WORD, font=('Yu Gothic', 10))
    text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
    text_widget.insert(tk.END, guide_text)
    text_widget.config(state=tk.DISABLED)

# 画像解析処理
def analyze_image():
    """画像を解析し、結果を表示"""
    if not llm:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', 'APIキーが設定されていません。')
        return

    if not img_path:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', '画像が選択されていません。')
        return

    question = q_text.get('1.0', tk.END).strip()
    if not question:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', '質問・指示が入力されていません。')
        return

    progress.start()
    r_text.delete('1.0', tk.END)
    r_text.insert('1.0', '解析中...')
    root.update()

    try:
        # Base64エンコード
        with open(img_path, 'rb') as image_file:
            encoded_image = base64.b64encode(image_file.read()).decode('utf-8')

        # MIMEタイプ取得
        ext = os.path.splitext(img_path)[1].lower()
        mime_types = {
            '.jpg': 'image/jpeg',
            '.jpeg': 'image/jpeg',
            '.png': 'image/png',
            '.gif': 'image/gif',
            '.bmp': 'image/bmp',
            '.tiff': 'image/tiff',
            '.tif': 'image/tiff',
            '.webp': 'image/webp'
        }
        mime_type = mime_types.get(ext, 'image/jpeg')

        message = HumanMessage(
            content=[
                {'type': 'text', 'text': question},
                {
                    'type': 'image_url',
                    'image_url': {'url': f'data:{mime_type};base64,{encoded_image}'}
                }
            ]
        )

        response = llm.invoke([message])
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', response.content)

        # 解析履歴保存
        timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        history_entry = f'''
{'='*80}
解析日時: {timestamp}
画像ファイル: {os.path.basename(img_path)}
使用モデル: {MODEL_NAME}
{'='*80}
質問:
{question}
{'-'*80}
解析結果:
{response.content}
{'='*80}

'''
        with open(RESULT_FILE, 'a', encoding='utf-8') as f:
            f.write(history_entry)

    except Exception as e:
        r_text.delete('1.0', tk.END)
        r_text.insert('1.0', f'解析エラー: {str(e)}')

    progress.stop()

# ウィンドウ終了処理
def on_closing():
    """アプリケーション終了時の処理"""
    current_result = r_text.get('1.0', tk.END).strip()
    if current_result and current_result != '解析中...':
        timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        session_end = f'''
{'='*80}
セッション終了: {timestamp}
{'='*80}

'''
        with open(RESULT_FILE, 'a', encoding='utf-8') as f:
            f.write(session_end)

    if os.path.exists(TEMP_IMAGE_FILE):
        try:
            os.remove(TEMP_IMAGE_FILE)
        except:
            pass

    root.destroy()

# GUI作成
root = tk.Tk()
root.title('Gemini API 画像解析ツール')
root.geometry(f'{WINDOW_WIDTH}x{WINDOW_HEIGHT}')
root.protocol('WM_DELETE_WINDOW', on_closing)

main_frame = ttk.Frame(root, padding='10')
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

# APIキー設定フレーム
api_frame = ttk.LabelFrame(main_frame, text='API設定', padding='5')
api_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

ttk.Label(api_frame, text='Google API Key:', font=('Yu Gothic', 10)).grid(row=0, column=0, sticky=tk.W)
api_key_var = tk.StringVar()
if api_key:
    api_key_var.set(api_key)
api_entry = ttk.Entry(api_frame, textvariable=api_key_var, width=API_ENTRY_WIDTH, show='*', font=('Yu Gothic', 10))
api_entry.grid(row=0, column=1, padx=(5, 0), sticky=(tk.W, tk.E))

api_button = tk.Button(api_frame, text='APIキー設定', command=set_api_key,
                      bg=COLOR_PRIMARY, fg='white', font=('Yu Gothic', 10, 'bold'),
                      cursor='hand2')
api_button.grid(row=0, column=2, padx=(5, 0))

# APIキー入手手順の表示
api_help_text = '''APIキー入手手順:
1. https://aistudio.google.com/app/apikey を開く
2. ブラウザでGoogleアカウントにログインしてください
3. 'Get API key'ボタンをクリック
4. 'Create API key'ボタンをクリック
5. 'Create API key in new project'をクリック
6. 表示されたAPIキーをコピーしてください'''

api_help_label = ttk.Label(api_frame, text=api_help_text, foreground='gray', font=('Yu Gothic', 9))
api_help_label.grid(row=1, column=0, columnspan=3, sticky=tk.W, pady=(5, 0))

# 画像選択フレーム
image_frame = ttk.LabelFrame(main_frame, text='画像選択', padding='5')
image_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))

select_button = tk.Button(image_frame, text='画像ファイルを選択', command=select_image,
                         bg=COLOR_SUCCESS, fg='white', font=('Yu Gothic', 10, 'bold'),
                         cursor='hand2')
select_button.grid(row=0, column=0)

camera_button = tk.Button(image_frame, text='カメラで撮影', command=capture_from_camera,
                         bg=COLOR_INFO, fg='white', font=('Yu Gothic', 10, 'bold'),
                         cursor='hand2')
camera_button.grid(row=0, column=1, padx=(5, 0))

img_path_var = tk.StringVar()
img_path_label = ttk.Label(image_frame, textvariable=img_path_var, foreground='blue', font=('Yu Gothic', 10))
img_path_label.grid(row=1, column=0, columnspan=2, padx=(0, 0), sticky=tk.W)

# 画像プレビューフレーム
preview_frame = ttk.LabelFrame(main_frame, text='画像プレビュー', padding='5')
preview_frame.grid(row=2, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))

image_label = ttk.Label(preview_frame, text='画像が選択されていません', font=('Yu Gothic', 10))
image_label.grid(row=0, column=0)

# ヒントフレーム
hint_frame = ttk.LabelFrame(main_frame, text='使い方のヒント', padding='5')
hint_frame.grid(row=3, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))

hint_text = scrolledtext.ScrolledText(hint_frame, height=8, width=30, font=('Yu Gothic', 9), wrap=tk.WORD)
hint_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

# 質問入力フレーム
question_frame = ttk.LabelFrame(main_frame, text='質問・指示', padding='5')
question_frame.grid(row=2, column=1, rowspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10), padx=(10, 0))

# プロンプト選択ボタンフレーム
prompt_button_frame = ttk.Frame(question_frame)
prompt_button_frame.grid(row=0, column=0, pady=(0, 5), sticky=(tk.W, tk.E))

learning_button = tk.Button(prompt_button_frame, text='学習支援', command=set_learning_support,
                           bg=COLOR_PRIMARY, fg='white', font=('Yu Gothic', 10, 'bold'),
                           cursor='hand2', relief=tk.SUNKEN)
learning_button.pack(side=tk.LEFT, padx=(0, 5))

free_button = tk.Button(prompt_button_frame, text='自由記入', command=set_free_input,
                       bg=COLOR_SECONDARY, fg='white', font=('Yu Gothic', 10, 'bold'),
                       cursor='hand2', relief=tk.RAISED)
free_button.pack(side=tk.LEFT, padx=(0, 5))

scene_button = tk.Button(prompt_button_frame, text='シーン分析', command=set_scene_analysis,
                        bg=COLOR_PURPLE, fg='white', font=('Yu Gothic', 10, 'bold'),
                        cursor='hand2', relief=tk.RAISED)
scene_button.pack(side=tk.LEFT, padx=(0, 5))

location_button = tk.Button(prompt_button_frame, text='場所別分析', command=set_location_analysis,
                           bg=COLOR_PINK, fg='white', font=('Yu Gothic', 10, 'bold'),
                           cursor='hand2', relief=tk.RAISED)
location_button.pack(side=tk.LEFT)

q_text = scrolledtext.ScrolledText(question_frame, height=QUESTION_HEIGHT, width=QUESTION_WIDTH, font=('Yu Gothic', 10))
q_text.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
q_text.insert('1.0', prompt_learn)

button_frame = ttk.Frame(question_frame)
button_frame.grid(row=2, column=0, pady=(5, 0), sticky=(tk.W, tk.E))

analyze_button = tk.Button(button_frame, text='画像を解析', command=analyze_image,
                          bg=COLOR_DANGER, fg='white', font=('Yu Gothic', 10, 'bold'),
                          cursor='hand2')
analyze_button.pack(side=tk.LEFT, padx=(0, 5))

v_button = tk.Button(button_frame, text='音声入力', command=voice_input,
                    bg=COLOR_WARNING, fg='black', font=('Yu Gothic', 10, 'bold'),
                    cursor='hand2')
v_button.pack(side=tk.LEFT)

guide_button = tk.Button(button_frame, text='使用方法', command=show_usage_guide,
                        bg=COLOR_SECONDARY, fg='white', font=('Yu Gothic', 10, 'bold'),
                        cursor='hand2')
guide_button.pack(side=tk.LEFT, padx=(5, 0))

v_status_var = tk.StringVar()
v_status_label = ttk.Label(button_frame, textvariable=v_status_var, foreground='green', font=('Yu Gothic', 10))
v_status_label.pack(side=tk.LEFT, padx=(10, 0))

# 結果表示フレーム
result_frame = ttk.LabelFrame(main_frame, text='解析結果', padding='5')
result_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))

r_text = scrolledtext.ScrolledText(result_frame, height=RESULT_HEIGHT, width=RESULT_WIDTH, font=('Yu Gothic', 10))
r_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

progress = ttk.Progressbar(main_frame, mode='indeterminate')
progress.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(5, 0))

# 注意書きラベル
notice_label = ttk.Label(main_frame,
                        text='※ このツールは学習サポートが目的です。最終的な理解は自分で確認しましょう。',
                        foreground='#666666',
                        font=('Yu Gothic', 9))
notice_label.grid(row=6, column=0, columnspan=2, pady=(5, 0))

# グリッド設定
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(2, weight=1)
main_frame.rowconfigure(3, weight=1)
main_frame.rowconfigure(4, weight=2)
api_frame.columnconfigure(1, weight=1)
result_frame.columnconfigure(0, weight=1)
result_frame.rowconfigure(0, weight=1)
question_frame.columnconfigure(0, weight=1)
question_frame.rowconfigure(1, weight=1)
hint_frame.columnconfigure(0, weight=1)
hint_frame.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

# APIキー自動設定
if api_key:
    set_api_key()

# 初期ヒント表示
update_hint(hint_learn)

# 使用方法表示
env_status = ''
if api_key:
    env_status = '\n※.envファイルからAPIキーが自動読み込みされました'

print('Gemini API 画像解析ツール')
print('1. Google AI StudioでAPIキーを取得してください')
print('2. APIキーを入力して「APIキー設定」をクリック')
print('   (.envファイルにGEMINI_API_KEY=を設定すると自動読み込み)')
print('3. 「画像ファイルを選択」で解析したい画像を選択、または')
print('   「カメラで撮影」でカメラから画像を撮影')
print('4. 質問や指示を入力(デフォルトの内容でも可)')
print('   - 「音声入力」ボタンで音声での入力も可能')
print('5. 「画像を解析」をクリックして結果を確認')
print('')
print('対応画像形式: JPG, PNG, GIF, BMP, TIFF, WebP')
print('※解析履歴は自動的にresult.txtに保存されます')
if env_status:
    print(env_status)

root.mainloop()