const DEFAULT_VISION_MODEL = process.env.GEMINI_VISION_MODEL || 'gemini-2.5-flash'; const GEMINI_MODEL = `https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_VISION_MODEL}:generateContent`; const CARD_PROMPT = `You are a specialized trading card recognition system. Analyze this image and determine if it contains a trading card (Magic: The Gathering, Pokemon, Yu-Gi-Oh, Lorcana, etc.). CRITICAL: Only respond with card data if you can clearly identify a TRADING CARD in the image. Ignore random objects, books, papers, phone screens, screenshots, blurry images, and non-card gaming items. If you detect a trading card, extract information in this JSON format: { "isCard": true, "cardName": "exact card name as printed on the card", "setName": "set name if visible", "setCode": "set code/symbol if visible", "cardNumber": "collector number if visible", "game": "mtg, pokemon, or lorcana (lowercase)", "cardType": "creature, instant, sorcery, trainer, etc.", "rarity": "common, uncommon, rare, mythic, etc.", "manaCost": "mana cost if visible", "hp": "HP or power if visible", "abilities": ["list of abilities or attacks if clearly readable"], "confidence": 85, "rawText": "all text visible on the card" } If NO trading card is clearly visible, respond with: { "isCard": false, "confidence": 0, "reason": "No trading card detected in image" } Be conservative — only extract data you can clearly read. Quality over quantity.`; function parseGeminiJson(content) { const cleanContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); try { return JSON.parse(cleanContent); } catch { const cardNameMatch = content.match(/card.*?name.*?[:"]\s*([^"'\n,}]+)/i); return { isCard: !!cardNameMatch, cardName: cardNameMatch ? cardNameMatch[1].trim() : null, confidence: 30, rawText: content, reason: 'Failed to parse structured response', }; } } function normalizeGame(game) { if (!game) return null; const value = String(game).trim().toLowerCase(); if (value === 'mtg' || value.includes('magic')) return 'mtg'; if (value.includes('pokemon') || value.includes('pokémon')) return 'pokemon'; if (value.includes('lorcana')) return 'lorcana'; return value; } /** * Server-side Gemini Vision analysis. Requires GEMINI_AI_API_KEY in env. * @param {string} imageDataUrl - data:image/jpeg;base64,... capture from scanner */ export async function analyzeCardImage(imageDataUrl) { const apiKey = process.env.GEMINI_AI_API_KEY; if (!apiKey) { throw new Error('GEMINI_AI_API_KEY is not configured on the server'); } const base64Data = imageDataUrl.split(',')[1]; if (!base64Data) { throw new Error('Invalid image data format'); } const response = await fetch(GEMINI_MODEL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey, }, body: JSON.stringify({ contents: [{ parts: [ { text: CARD_PROMPT }, { inline_data: { mime_type: 'image/jpeg', data: base64Data, }, }, ], }], generationConfig: { temperature: 0.1, }, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( `Gemini API error: ${response.status} - ${errorData.error?.message || 'Unknown error'}` ); } const data = await response.json(); const content = data.candidates?.[0]?.content?.parts?.[0]?.text; if (!content) { throw new Error('No response from Gemini API'); } const result = parseGeminiJson(content); return { isCard: result.isCard || false, cardName: result.cardName || null, setName: result.setName || null, setCode: result.setCode || null, cardNumber: result.cardNumber || null, game: normalizeGame(result.game), cardType: result.cardType || null, rarity: result.rarity || null, manaCost: result.manaCost || null, hp: result.hp || null, abilities: result.abilities || [], confidence: result.confidence || 0, rawText: result.rawText || content, reason: result.reason || null, }; }