ทำเว็บอธิบายรูปด้วย Gemini Pro Vision + Next.js (App Router) ง่ายๆ บน Vercel
ไหนๆ ช่วงนี้ AI ก็มาแรงเหลือเกิน วันนี้เลยอยากพามาลองเล่น Gemini Pro Vision กันหน่อย คือมันเจ๋งตรงที่สามารถให้ AI มันช่วยอธิบายรูปที่เราอัพโหลดเข้าไปได้เลยนะ เหมาะมากสำหรับเอาไปทำฟีเจอร์เจ๋งๆ หรือแค่ลองเล่นขำๆ วันนี้เราจะใช้ Next.js App Router มาเป็นตัวขับเคลื่อน แล้วเอาขึ้น Vercel ให้มันจบๆ ไปเลย
ขั้นตอนไม่ได้ซับซ้อนมาก ถ้าคุ้นเคยกับ JavaScript/TypeScript มาบ้าง ก็น่าจะไปได้ไวเลยล่ะ
เตรียมโปรเจกต์ Next.js
อันดับแรกก็ต้องสร้างโปรเจกต์ Next.js ขึ้นมาก่อนเลยนะ ใช้คำสั่งนี้ได้เลย
pnpm create next-app nextjs-gemini-vision --ts --app --tailwind --eslint
# หรือ npm, yarn ก็ได้ แล้วแต่ถนัดนะ
พอสร้างเสร็จ ก็เข้าไปในโฟลเดอร์โปรเจกต์ แล้วติดตั้ง @google/generative-ai SDK กันต่อ
cd nextjs-gemini-vision
pnpm add @google/generative-ai
อย่าลืมไปสร้าง GEMINI_API_KEY ที่ Google AI Studio ด้วยนะ แล้วเอา Key ที่ได้มาใส่ในไฟล์ .env.local แบบนี้:
GEMINI_API_KEY=YOUR_API_KEY_ตรงนี้
สร้าง API Route สำหรับเชื่อม Gemini
ทีนี้เราจะมาสร้าง API Endpoint ให้ฝั่ง Frontend เรียกใช้กัน โดยจะสร้างใน app/api/describe-image/route.ts นะ
// app/api/describe-image/route.ts
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from '@google/generative-ai';
import { NextResponse } from 'next/server';
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
export async function POST(req: Request) {
try {
const { imageData } = await req.json(); // เราจะส่ง base64 string มาตรงนี้
if (!imageData) {
return NextResponse.json({ error: 'ไม่เจอข้อมูลรูปภาพนะ' }, { status: 400 });
}
// แปลง base64 string ให้เป็น parts ที่ Gemini เข้าใจ
const imageParts = [{
inlineData: {
data: imageData.split(',')[1], // ตัด "data:image/jpeg;base64," ออกไป
mimeType: imageData.split(',')[0].split(':')[1].split(';')[0] // ดึง mime type
}
}];
const model = genAI.getGenerativeModel({ model: "gemini-pro-vision" });
// กำหนด safety settings นิดนึง กัน AI ตอบอะไรแปลกๆ
const safetySettings = [
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_NONE,
},
// ... กำหนด categories อื่นๆ ได้ตามต้องการนะ
];
const prompt = "รูปนี้มีอะไรบ้าง?"; // คำสั่งง่ายๆ อยากให้ AI อธิบายอะไรก็ใส่ตรงนี้เลย
const result = await model.generateContent([prompt, ...imageParts], { safetySettings });
const response = await result.response;
const text = response.text();
return NextResponse.json({ description: text }, { status: 200 });
} catch (error) {
console.error('Error calling Gemini API:', error);
// เรื่อง error ตรงนี้นี่แหละที่ปวดหัว บางที Base64 มันใหญ่ไป
// เคยเจอตอนรูปมันใหญ่มากๆ แล้วส่งเป็น JSON payload ไปตรงๆ
// มันจะเจอ error "413 Payload Too Large" ซึ่งอันนี้ต้องไปแก้ที่ฝั่ง client ให้บีบรูป
// หรือไม่ก็ต้องพิจารณาส่งแบบ multi-part form data ซึ่งมันยุ่งยากกว่าในบทความนี้
// เอาเป็นว่า ถ้ามันเป็น error จาก Gemini เอง ก็ให้มันบอกไปตรงๆ
if (error instanceof Error) {
return NextResponse.json({ error: 'มีปัญหาบางอย่างกับ Gemini API หรือข้อมูลรูปภาพ: ' + error.message }, { status: 500 });
}
return NextResponse.json({ error: 'เกิดข้อผิดพลาดไม่ทราบสาเหตุ' }, { status: 500 });
}
}
ความคิดเห็นส่วนตัว: ตอนแรกที่ลองทำตรงนี้คือปวดหัวกับเรื่องขนาดไฟล์รูปที่แปลงเป็น Base64 แล้วส่งไปใน JSON payload มากๆ เลยนะ เพราะถ้ามันใหญ่เกินไปนิดเดียวก็โดน 413 Payload Too Large ทันที วิธีแก้ก็คือต้องไปบีบรูปที่ฝั่ง Frontend ก่อนส่ง หรือไม่ก็เปลี่ยนไปใช้ FormData ส่งแบบ multipart/form-data ซึ่งมันดูยุ่งยากกว่าสำหรับตัวอย่างง่ายๆ แบบนี้อะนะ ฉะนั้นตัวอย่างนี้ก็เลยเน้นแค่ให้มันทำงานได้ง่ายๆ ไปก่อน
สร้างหน้า Frontend สำหรับอัพโหลดและแสดงผล
มาที่ไฟล์ app/page.tsx ของเรากัน ทีนี้เราจะสร้าง Component สำหรับให้อัพโหลดรูป และแสดงผลลัพธ์ที่ได้จาก Gemini กัน
// app/page.tsx
'use client'; // ต้องมีอันนี้นะ ถ้าจะใช้ client-side hook
import { useState } from 'react';
export default function HomePage() {
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [description, setDescription] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files[0]) {
const file = event.target.files[0];
setSelectedImage(file);
setImageUrl(URL.createObjectURL(file)); // สร้าง URL สำหรับแสดงรูป
setDescription(null); // เคลียร์คำอธิบายเก่า
setError(null); // เคลียร์ error
}
};
const handleDescribeImage = async () => {
if (!selectedImage) {
setError('กรุณาเลือกรูปภาพก่อนนะ');
return;
}
setLoading(true);
setError(null);
setDescription(null);
const reader = new FileReader();
reader.readAsDataURL(selectedImage); // แปลงรูปเป็น Base64
reader.onloadend = async () => {
try {
const base64Image = reader.result as string;
// ตรงนี้จะเรียก API Route ที่เราสร้างไว้
const response = await fetch('/api/describe-image', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ imageData: base64Image }),
});
const data = await response.json();
if (response.ok) {
setDescription(data.description);
} else {
setError(data.error || 'เกิดข้อผิดพลาดบางอย่าง');
}
} catch (err) {
console.error('Fetch error:', err);
setError('ไม่สามารถเชื่อมต่อกับ Server ได้ ลองใหม่อีกทีนะ');
} finally {
setLoading(false);
}
};
reader.onerror = () => {
setError('ไม่สามารถอ่านไฟล์รูปภาพได้');
setLoading(false);
};
};
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg text-center">
<h1 className="text-2xl font-bold mb-4">ให้ Gemini อธิบายรูปให้!</h1>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="mb-4 block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-violet-50 file:text-violet-700
hover:file:bg-violet-100"
/>
{imageUrl && (
<div className="mb-4">
<img src={imageUrl} alt="Preview" className="max-w-full h-auto rounded-lg mx-auto" />
</div>
)}
<button
onClick={handleDescribeImage}
disabled={!selectedImage || loading}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg
disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
>
{loading ? 'กำลังคิด...' : 'อธิบายรูปนี้ให้หน่อย!'}
</button>
{error && (
<p className="mt-4 text-red-600 font-medium">{error}</p>
)}
{description && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200 text-left">
<h2 className="text-lg font-semibold mb-2">Gemini บอกว่า:</h2>
<p className="text-gray-800 whitespace-pre-wrap">{description}</p>
</div>
)}
</div>
</div>
);
}
ลอง pnpm dev แล้วเข้า http://localhost:3000 ดูก็จะเจอหน้าเว็บให้ลองอัพโหลดรูปแล้ว!
Deploy ขึ้น Vercel
ส่วนการเอาขึ้น Vercel นี่คือแทบจะง่ายที่สุดแล้วววว 1. Push code ขึ้น GitHub/GitLab/Bitbucket. 2. ไปที่ Vercel (ถ้ายังไม่มี Account ก็สมัครเลยฟรีๆ) แล้ว Import Git Repository ของเรา. 3. สำคัญมาก! อย่าลืมตั้งค่า GEMINI_API_KEY เป็น Environment Variable บน Vercel ด้วยนะ (ไปที่ Project Settings -> Environment Variables แล้วเพิ่ม Key เข้าไป) 4. กด Deploy! แค่นี้ก็เรียบร้อยแล้ว เว็บเราก็จะออนไลน์ให้คนอื่นได้ใช้ทันที
สรุป
เห็นมั้ยว่าการเอา AI มาใส่ในเว็บของเรามันไม่ได้ยากอย่างที่คิดเลยนะ แถมยังทำได้ไวมากๆ ด้วย Next.js App Router และ Vercel ที่ช่วยให้เราโฟกัสกับโค้ดจริงๆ ไม่ต้องมานั่ง Config อะไรให้ปวดหัวเยอะ ใครสนใจก็ลองเอาไปต่อยอดทำอะไรเจ๋งๆ กันได้เลย!