2026-05-27 13:58:16 -04:00
const GATEWAY _URL = 'https://ai-gateway.vercel.sh/v1/chat/completions' ;
const DEFAULT _VISION _MODEL =
process . env . SCAN _VISION _MODEL || 'google/gemini-2.5-flash-lite' ;
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 . ` ;
2026-05-27 13:58:16 -04:00
function parseVisionJson ( content ) {
2026-05-27 09:47:05 -04:00
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 ;
}
/ * *
2026-05-27 13:58:16 -04:00
* Server - side vision analysis via Vercel AI Gateway . Requires AI _GATEWAY _API _KEY .
2026-05-27 09:47:05 -04:00
* @ param { string } imageDataUrl - data : image / jpeg ; base64 , ... capture from scanner
* /
export async function analyzeCardImage ( imageDataUrl ) {
2026-05-27 13:58:16 -04:00
const apiKey = process . env . AI _GATEWAY _API _KEY ;
2026-05-27 09:47:05 -04:00
if ( ! apiKey ) {
2026-05-27 13:58:16 -04:00
throw new Error ( 'AI_GATEWAY_API_KEY is not configured on the server' ) ;
2026-05-27 09:47:05 -04:00
}
2026-05-27 13:58:16 -04:00
if ( ! imageDataUrl ? . includes ( ',' ) ) {
2026-05-27 09:47:05 -04:00
throw new Error ( 'Invalid image data format' ) ;
}
2026-05-27 13:58:16 -04:00
const response = await fetch ( GATEWAY _URL , {
2026-05-27 09:47:05 -04:00
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
2026-05-27 13:58:16 -04:00
Authorization : ` Bearer ${ apiKey } ` ,
2026-05-27 09:47:05 -04:00
} ,
body : JSON . stringify ( {
2026-05-27 13:58:16 -04:00
model : DEFAULT _VISION _MODEL ,
temperature : 0.1 ,
messages : [ {
role : 'user' ,
content : [
{ type : 'text' , text : CARD _PROMPT } ,
{ type : 'image_url' , image _url : { url : imageDataUrl } } ,
2026-05-27 09:47:05 -04:00
] ,
} ] ,
} ) ,
} ) ;
if ( ! response . ok ) {
const errorData = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
2026-05-27 13:58:16 -04:00
const message = errorData . error ? . message || errorData . message || 'Unknown error' ;
throw new Error ( ` Vision API error: ${ response . status } - ${ message } ` ) ;
2026-05-27 09:47:05 -04:00
}
const data = await response . json ( ) ;
2026-05-27 13:58:16 -04:00
const content = data . choices ? . [ 0 ] ? . message ? . content ;
2026-05-27 09:47:05 -04:00
if ( ! content ) {
2026-05-27 13:58:16 -04:00
throw new Error ( 'No response from vision model' ) ;
2026-05-27 09:47:05 -04:00
}
2026-05-27 13:58:16 -04:00
const result = parseVisionJson ( content ) ;
2026-05-27 09:47:05 -04:00
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 ,
} ;
}