2026-05-27 13:41:02 -04:00
const DEFAULT _VISION _MODEL = process . env . GEMINI _VISION _MODEL || 'gemini-2.5-flash' ;
2026-05-27 09:47:05 -04:00
const GEMINI _MODEL =
2026-05-27 13:41:02 -04:00
` https://generativelanguage.googleapis.com/v1beta/models/ ${ DEFAULT _VISION _MODEL } :generateContent ` ;
2026-05-27 09:47:05 -04:00
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 ,
} ;
}