Gemini API キー,LangChain による画像QA(ソースコードと実行結果)

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 langchain-core opencv-python pillow SpeechRecognition pyaudio

Gemini API キー,LangChain による画像QAプログラム

概要

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

ソースコード


"""
プログラム名: Gemini API画像解析ツール
特徴技術名: Google Gemini API (gemini-2.5-flash)
出典: Google AI Studio (2024). Gemini API Documentation. https://ai.google.dev/
特徴機能: マルチモーダル画像理解機能 - 画像とテキストプロンプトを同時に処理し、画像の内容を自然言語で詳細に説明
学習済みモデル: なし(クラウドベースAPIを使用)
方式設計:
  - 関連利用技術:
    - LangChain: LLM統合フレームワーク、Gemini APIとの連携を簡素化
    - OpenCV: カメラ制御と画像キャプチャ
    - Speech Recognition: Google Speech APIによる音声入力
    - Tkinter: クロスプラットフォームGUI
  - 入力と出力: 入力: 静止画像(ユーザは「画像ファイルを選択」または「カメラで撮影」で選択)、出力: テキスト(解析結果)
  - 処理手順: 1)画像取得 2)Base64エンコード 3)質問と画像をGemini APIに送信 4)解析結果を表示・保存
  - 前処理、後処理: 前処理: 画像のBase64エンコード、後処理: 解析履歴のファイル保存
  - 追加処理: なし
  - 調整を必要とする設定値: GEMINI_API_KEY(Google AI StudioのAPIキー)
将来方策: APIキーの自動検証機能を実装し、無効なキーの場合は即座にフィードバック
その他の重要事項: Windows環境での動作を想定、.envファイルからのAPIキー自動読み込み対応
前準備:
  - pip install langchain-google-genai langchain-core opencv-python pillow SpeechRecognition pyaudio
"""

import base64
import datetime
import os
import threading
import tkinter as tk
from tkinter import ttk, filedialog, scrolledtext

import cv2
import speech_recognition as sr
from PIL import Image, ImageTk
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage

# 設定定数
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'
API_KEY_PREFIX = 'GEMINI_API_KEY='

# メッセージ定数
ANALYZING_MESSAGE = '解析中...'
IMAGE_NOT_SELECTED = '画像が選択されていません'
CAPTURED_IMAGE_TEXT = '撮影した画像'

# グローバル変数
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

# .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(API_KEY_PREFIX):
                        api_key = line.split('=', 1)[1].strip()
    except Exception as e:
        print(f'{ENV_FILE}ファイルの読み込みに失敗しました: {str(e)}')


# APIキー設定処理
def set_api_key():
    global api_key, llm
    api_key = api_key_var.get().strip()
    if not api_key:
        return
    os.environ['GOOGLE_API_KEY'] = api_key
    llm = ChatGoogleGenerativeAI(
        model='gemini-2.5-flash',
        temperature=0
    )


# 画像プレビュー更新処理
def update_image_preview(image_path):
    image = Image.open(image_path)
    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


# カメラ撮影処理
def capture_from_camera():
    global img_path
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        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(CAPTURED_IMAGE_TEXT)
            update_image_preview(TEMP_IMAGE_FILE)
            break
        elif key == ord('q'):
            break

    cap.release()
    cv2.destroyAllWindows()


# 画像ファイル選択処理
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))
        update_image_preview(filename)


# 音声入力スレッド処理
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.WaitTimeoutError:
        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 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():
    global recognizer, microphone
    if recognizer is None:
        recognizer = sr.Recognizer()
        microphone = sr.Microphone()

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


# 画像解析処理
def analyze_image():
    if not llm:
        return

    if not img_path:
        return

    question = q_text.get('1.0', tk.END).strip()
    if not question:
        return

    progress.start()
    r_text.delete('1.0', tk.END)
    r_text.insert('1.0', ANALYZING_MESSAGE)
    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) if img_path else '未選択'}
質問: {question}
{'='*80}
解析結果:
{response.content}

"""
        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)}')
        print(f'Gemini API呼び出しに失敗しました: {str(e)}')

    progress.stop()


# ウィンドウ終了処理
def on_closing():
    current_result = r_text.get('1.0', tk.END).strip()
    if current_result and current_result != ANALYZING_MESSAGE:
        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):
        os.remove(TEMP_IMAGE_FILE)

    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:').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='*')
api_entry.grid(row=0, column=1, padx=(5, 0), sticky=(tk.W, tk.E))

ttk.Button(api_frame, text='APIキー設定',
           command=set_api_key).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')
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))

ttk.Button(image_frame, text='画像ファイルを選択',
           command=select_image).grid(row=0, column=0)
ttk.Button(image_frame, text='カメラで撮影',
           command=capture_from_camera).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')
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=IMAGE_NOT_SELECTED)
image_label.grid(row=0, column=0)

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

q_text = scrolledtext.ScrolledText(question_frame, height=QUESTION_HEIGHT,
                                   width=QUESTION_WIDTH)
q_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
q_text.insert('1.0', 'この画像について詳しく説明してください。')

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

ttk.Button(button_frame, text='画像を解析',
           command=analyze_image).pack(side=tk.LEFT, padx=(0, 5))

v_button = ttk.Button(button_frame, text='音声入力', command=voice_input)
v_button.pack(side=tk.LEFT)

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

# 結果表示フレーム
result_frame = ttk.LabelFrame(main_frame, text='解析結果', padding='5')
result_frame.grid(row=3, 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)
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=4, column=0, columnspan=2, sticky=(tk.W, tk.E), 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=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(0, weight=1)
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

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

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

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

root.mainloop()