// AI OCR Service for Trading Card Recognition export class AICardOCR { constructor() { this.apiKey = null; this.baseUrl = 'https://api.openai.com/v1'; } setApiKey(apiKey) { this.apiKey = apiKey; } async analyzeCard(imageDataUrl) { if (!this.apiKey) { throw new Error('OpenAI API key not configured'); } const 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.). IMPORTANT: Only respond with card data if you can clearly identify a TRADING CARD in the image. Ignore: - Random objects, books, papers - Screenshots of websites or apps - Blurry or unclear images - Non-card gaming items If you detect a trading card, extract the following information in JSON format: { "isCard": true, "cardName": "exact card name as printed", "setName": "set name if visible", "setCode": "set code/symbol if visible", "cardNumber": "collector number if visible", "game": "MTG, Pokemon, YuGiOh, Lorcana, etc.", "cardType": "creature, instant, trainer, etc.", "rarity": "common, uncommon, rare, mythic, etc.", "manaCost": "mana cost if visible", "hp": "HP or power if visible", "abilities": ["list of abilities/attacks if visible"], "flavorText": "flavor text if clearly readable", "artist": "artist name if visible", "confidence": 85, "rawText": "all visible text on the card" } If NO trading card is detected, respond with: { "isCard": false, "confidence": 0, "reason": "No trading card detected in image" } Focus on accuracy over speed. Only extract data you can clearly read.`; try { const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [ { role: 'user', content: [ { type: 'text', text: prompt }, { type: 'image_url', image_url: { url: imageDataUrl, detail: 'high' } } ] } ], max_tokens: 1000, temperature: 0.1 }) }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status}`); } const data = await response.json(); const content = data.choices[0]?.message?.content; if (!content) { throw new Error('No response from OpenAI'); } // Parse JSON response let result; try { // Clean up the response - remove markdown code blocks if present const cleanContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); result = JSON.parse(cleanContent); } catch (parseError) { console.error('Failed to parse OpenAI JSON response:', content); // Fallback: try to extract card name from raw text const cardNameMatch = content.match(/card.*?name.*?[:"]\s*([^"'\n]+)/i); result = { isCard: !!cardNameMatch, cardName: cardNameMatch ? cardNameMatch[1].trim() : null, confidence: 30, rawText: content, reason: 'Failed to parse structured response' }; } // Ensure we have the required structure return { isCard: result.isCard || false, cardName: result.cardName || null, setName: result.setName || null, setCode: result.setCode || null, cardNumber: result.cardNumber || null, game: result.game || null, cardType: result.cardType || null, rarity: result.rarity || null, manaCost: result.manaCost || null, hp: result.hp || null, abilities: result.abilities || [], flavorText: result.flavorText || null, artist: result.artist || null, confidence: result.confidence || 0, rawText: result.rawText || content, reason: result.reason || null }; } catch (error) { console.error('OpenAI Vision API error:', error); throw error; } } } export class OllamaVisionOCR { constructor() { this.baseUrl = 'http://localhost:11434'; } setBaseUrl(url) { this.baseUrl = url; } async analyzeCard(imageDataUrl) { const 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.). IMPORTANT: Only respond with card data if you can clearly identify a TRADING CARD in the image. Ignore random objects, books, papers, screenshots, or blurry images. If you detect a trading card, extract this information in JSON format: { "isCard": true, "cardName": "exact card name as printed", "setName": "set name if visible", "setCode": "set code if visible", "cardNumber": "collector number if visible", "game": "MTG, Pokemon, YuGiOh, Lorcana, etc.", "cardType": "creature, instant, trainer, etc.", "rarity": "common, uncommon, rare, mythic, etc.", "confidence": 85, "rawText": "all visible text" } If NO trading card detected, respond: {"isCard": false, "confidence": 0, "reason": "No trading card detected"}`; try { // Convert data URL to base64 const base64Data = imageDataUrl.split(',')[1]; const response = await fetch(`${this.baseUrl}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'llava:latest', prompt: prompt, images: [base64Data], stream: false, options: { temperature: 0.1, top_p: 0.9 } }) }); if (!response.ok) { throw new Error(`Ollama API error: ${response.status}`); } const data = await response.json(); const content = data.response; if (!content) { throw new Error('No response from Ollama'); } // Parse JSON response let result; try { const cleanContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); result = JSON.parse(cleanContent); } catch (parseError) { console.error('Failed to parse Ollama JSON response:', content); result = { isCard: false, confidence: 0, rawText: content, reason: 'Failed to parse response' }; } return { isCard: result.isCard || false, cardName: result.cardName || null, setName: result.setName || null, setCode: result.setCode || null, cardNumber: result.cardNumber || null, game: result.game || null, cardType: result.cardType || null, rarity: result.rarity || null, manaCost: result.manaCost || null, hp: result.hp || null, abilities: result.abilities || [], flavorText: result.flavorText || null, artist: result.artist || null, confidence: result.confidence || 0, rawText: result.rawText || content, reason: result.reason || null }; } catch (error) { console.error('Ollama Vision API error:', error); throw error; } } } export class PuterVisionOCR { constructor() { this.puterLoaded = false; this.authFailed = false; } async loadPuterJS() { if (this.puterLoaded || typeof window === 'undefined') return; return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://js.puter.com/v2/'; script.onload = () => { this.puterLoaded = true; resolve(); }; script.onerror = reject; document.head.appendChild(script); }); } async analyzeCard(imageDataUrl) { // If we've already failed auth, don't try again if (this.authFailed) { throw new Error('Puter.js authentication failed. Please use OpenAI or Ollama instead.'); } try { await this.loadPuterJS(); if (!window.puter) { throw new Error('Puter.js not loaded'); } // Check if user is authenticated with Puter try { await window.puter.auth.getUser(); } catch (authError) { console.warn('Puter.js authentication required. Please sign in to Puter.com first.'); this.authFailed = true; throw new Error('Puter.js requires authentication. Please use OpenAI or Ollama instead, or sign in to Puter.com first.'); } const 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 of websites or digital interfaces - Blurry, unclear, or dark images - Non-card gaming items or accessories 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, YuGiOh, Lorcana, etc.", "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.`; const response = await window.puter.ai.chat(prompt, imageDataUrl, { model: "gpt-4o" }); if (!response) { throw new Error('No response from Puter.js'); } // Parse JSON response let result; try { // Clean up the response - remove markdown code blocks if present const cleanContent = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); result = JSON.parse(cleanContent); } catch (parseError) { console.error('Failed to parse Puter JSON response:', response); // Try to extract card name from raw response const cardNameMatch = response.match(/card.*?name.*?[:"]\s*([^"'\n,}]+)/i); result = { isCard: !!cardNameMatch, cardName: cardNameMatch ? cardNameMatch[1].trim() : null, confidence: 30, rawText: response, reason: 'Failed to parse structured response' }; } // Ensure we have the required structure return { isCard: result.isCard || false, cardName: result.cardName || null, setName: result.setName || null, setCode: result.setCode || null, cardNumber: result.cardNumber || null, game: result.game || null, cardType: result.cardType || null, rarity: result.rarity || null, manaCost: result.manaCost || null, hp: result.hp || null, abilities: result.abilities || [], flavorText: result.flavorText || null, artist: result.artist || null, confidence: result.confidence || 0, rawText: result.rawText || response, reason: result.reason || null }; } catch (error) { console.error('Puter.js Vision API error:', error); // Mark auth as failed if it's an auth-related error if (error.message.includes('authentication') || error.message.includes('auth') || error.message.includes('401')) { this.authFailed = true; } throw error; } } } // Gemini Vision OCR using Google's Gemini API export class GeminiVisionOCR { constructor() { this.apiKey = null; } setApiKey(apiKey) { this.apiKey = apiKey; } async analyzeCard(imageDataUrl) { if (!this.apiKey) { throw new Error('Gemini API key not configured'); } // Convert data URL to base64 const base64Data = imageDataUrl.split(',')[1]; if (!base64Data) { throw new Error('Invalid image data format'); } const 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 of websites or digital interfaces - Blurry, unclear, or dark images - Non-card gaming items or accessories 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, YuGiOh, Lorcana, etc.", "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.`; try { const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': this.apiKey }, body: JSON.stringify({ contents: [{ parts: [ { text: prompt }, { inline_data: { mime_type: 'image/jpeg', data: base64Data } } ] }], generationConfig: { thinkingConfig: { thinkingBudget: 0 // Disable thinking for faster response } } }) }); 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'); } // Parse JSON response let result; try { // Clean up the response - remove markdown code blocks if present const cleanContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); result = JSON.parse(cleanContent); } catch (parseError) { console.error('Failed to parse Gemini JSON response:', content); // Try to extract card name from raw response const cardNameMatch = content.match(/card.*?name.*?[:"]\s*([^"'\n,}]+)/i); result = { isCard: !!cardNameMatch, cardName: cardNameMatch ? cardNameMatch[1].trim() : null, confidence: 30, rawText: content, reason: 'Failed to parse structured response' }; } // Ensure we have the required structure return { isCard: result.isCard || false, cardName: result.cardName || null, setName: result.setName || null, setCode: result.setCode || null, cardNumber: result.cardNumber || null, game: result.game || null, cardType: result.cardType || null, rarity: result.rarity || null, manaCost: result.manaCost || null, hp: result.hp || null, abilities: result.abilities || [], flavorText: result.flavorText || null, artist: result.artist || null, confidence: result.confidence || 0, rawText: result.rawText || content, reason: result.reason || null }; } catch (error) { console.error('Gemini Vision API error:', error); throw error; } } } // Export singleton instances export const aiCardOCR = new AICardOCR(); export const ollamaCardOCR = new OllamaVisionOCR(); export const puterCardOCR = new PuterVisionOCR(); export const geminiCardOCR = new GeminiVisionOCR();