0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Geminiで飯食ってるか判定するアプリをつくーる

0
Last updated at Posted at 2025-12-24

はじめに

飯食ってるか判定するアプリを作ってもらいました

開発環境

  • Gemini

導入

Geminiに聞いてみましょう

💬 画像をキャプチャして飯食ってるか判定するアプリ作って

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>飯食ってるか判定AI</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        .loading-spinner {
            border: 4px solid rgba(0, 0, 0, 0.1);
            width: 36px;
            height: 36px;
            border-radius: 50%;
            border-left-color: #3b82f6;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col items-center p-4">

    <div class="max-w-md w-full bg-white rounded-2xl shadow-xl overflow-hidden">
        <!-- Header -->
        <div class="bg-blue-600 p-6 text-white text-center">
            <h1 class="text-2xl font-bold">飯食ってるか判定AI</h1>
            <p class="text-blue-100 text-sm mt-1">カメラを向けて判定ボタンを押してね</p>
        </div>

        <!-- Camera Preview -->
        <div class="relative aspect-video bg-black flex items-center justify-center overflow-hidden">
            <video id="webcam" autoplay playsinline class="w-full h-full object-cover"></video>
            <canvas id="canvas" class="hidden"></canvas>
            
            <!-- Result Overlay -->
            <div id="loadingOverlay" class="absolute inset-0 bg-black/50 hidden flex-col items-center justify-center text-white">
                <div class="loading-spinner mb-4"></div>
                <p>AIがもぐもぐ判定中...</p>
            </div>
        </div>

        <!-- Controls -->
        <div class="p-6 space-y-4">
            <button id="captureBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-4 rounded-xl transition-all active:scale-95 shadow-lg flex items-center justify-center gap-2">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
                判定する!
            </button>

            <!-- Result Card -->
            <div id="resultCard" class="hidden border-2 border-blue-100 bg-blue-50 rounded-xl p-4 animate-in fade-in duration-500">
                <div class="flex items-center gap-2 mb-2">
                    <span id="statusBadge" class="px-3 py-1 rounded-full text-xs font-bold uppercase"></span>
                    <h3 class="font-bold text-gray-800">判定結果</h3>
                </div>
                <p id="resultText" class="text-gray-700 text-sm leading-relaxed"></p>
            </div>
        </div>
    </div>

    <div class="mt-8 text-gray-400 text-xs text-center">
        Powered by Gemini 2.5 Flash<br>
        ※カメラの使用を許可してください
    </div>

    <script>
        const video = document.getElementById('webcam');
        const canvas = document.getElementById('canvas');
        const captureBtn = document.getElementById('captureBtn');
        const loadingOverlay = document.getElementById('loadingOverlay');
        const resultCard = document.getElementById('resultCard');
        const resultText = document.getElementById('resultText');
        const statusBadge = document.getElementById('statusBadge');

        const apiKey = ""; // 実行環境から提供されます

        // カメラのセットアップ
        async function setupCamera() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ 
                    video: { facingMode: 'user' }, 
                    audio: false 
                });
                video.srcObject = stream;
            } catch (err) {
                console.error("Camera error:", err);
                alert("カメラにアクセスできません。設定を確認してください。");
            }
        }

        // 指数バックオフ付きのAPI呼び出し
        async function fetchGemini(payload, retries = 5) {
            const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
            
            for (let i = 0; i < retries; i++) {
                try {
                    const response = await fetch(url, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(payload)
                    });
                    if (response.ok) return await response.json();
                } catch (err) {
                    if (i === retries - 1) throw err;
                }
                await new Promise(res => setTimeout(res, Math.pow(2, i) * 1000));
            }
        }

        // 画像をキャプチャして判定
        captureBtn.addEventListener('click', async () => {
            // UI更新
            loadingOverlay.classList.remove('hidden');
            resultCard.classList.add('hidden');
            captureBtn.disabled = true;

            // Canvasに描画
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

            // Base64に変換
            const base64Image = canvas.toDataURL('image/jpeg').split(',')[1];

            const prompt = "この画像に写っている人は何かを食べていますか?「食べている」か「食べていない」かを判断し、もし食べているなら何を食べているか、どんな様子か(美味しそう、急いでいる等)をフレンドリーな日本語で短く説明してください。返答の最初に必ず【食事中】または【食事なし】と付けてください。";

            const payload = {
                contents: [{
                    parts: [
                        { text: prompt },
                        { inlineData: { mimeType: "image/jpeg", data: base64Image } }
                    ]
                }]
            };

            try {
                const result = await fetchGemini(payload);
                const text = result.candidates?.[0]?.content?.parts?.[0]?.text || "判定できませんでした。";
                
                displayResult(text);
            } catch (error) {
                console.error(error);
                resultText.innerText = "エラーが発生しました。もう一度試してください。";
                resultCard.classList.remove('hidden');
            } finally {
                loadingOverlay.classList.add('hidden');
                captureBtn.disabled = false;
            }
        });

        function displayResult(text) {
            resultCard.classList.remove('hidden');
            
            if (text.includes("【食事中】")) {
                statusBadge.innerText = "食事中";
                statusBadge.className = "px-3 py-1 rounded-full text-xs font-bold uppercase bg-green-500 text-white";
                resultText.innerText = text.replace("【食事中】", "").trim();
            } else {
                statusBadge.innerText = "食事なし";
                statusBadge.className = "px-3 py-1 rounded-full text-xs font-bold uppercase bg-gray-400 text-white";
                resultText.innerText = text.replace("【食事なし】", "").trim() || "今は食べていないようです。";
            }
        }

        window.onload = setupCamera;
    </script>
</body>
</html>

一発でできました

20251224-064300-00733184.png

20251224-064358-cdd1aa81.png

お疲れ様でした

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?