diff --git a/src/services/aiOcr.ts b/src/services/aiOcr.ts index bf7f05c..0cb7c24 100644 --- a/src/services/aiOcr.ts +++ b/src/services/aiOcr.ts @@ -46,23 +46,7 @@ class AICardOCR { content: [ { type: 'text', - text: `Analyze this trading card image and extract the following information in JSON format: - -{ - "cardName": "exact card name", - "setName": "set name if visible", - "setCode": "3-4 letter set code if visible", - "cardType": "creature, spell, pokemon, etc", - "rarity": "common, uncommon, rare, etc", - "hp": "HP value for Pokemon", - "attacks": ["attack names"], - "abilities": ["ability names"], - "rawText": "all visible text on the card", - "game": "MTG or POKEMON or LORCANA", - "confidence": 95 -} - -Focus on accuracy. If you can't clearly read something, leave it empty rather than guessing.` + text: 'Analyze this trading card image and extract information. Respond with ONLY valid JSON, no markdown formatting or code blocks:\n\n{"cardName": "exact card name", "setName": "set name if visible", "setCode": "3-4 letter set code if visible", "cardType": "creature, spell, pokemon, etc", "rarity": "common, uncommon, rare, etc", "hp": "HP value for Pokemon", "attacks": ["attack names"], "abilities": ["ability names"], "rawText": "all visible text on the card", "game": "MTG or POKEMON or LORCANA", "confidence": 95}\n\nIMPORTANT: Return ONLY the JSON object, no markdown code blocks. Focus on accuracy, leave fields empty if unclear. For the cardName, use the exact name as printed on the card.' }, { type: 'image_url', @@ -90,9 +74,17 @@ Focus on accuracy. If you can't clearly read something, leave it empty rather th throw new Error('No content received from OpenAI'); } - // Parse JSON response + // Clean and parse JSON response try { - const result = JSON.parse(content); + // Remove markdown code blocks if present + let cleanContent = content.trim(); + if (cleanContent.startsWith('```json')) { + cleanContent = cleanContent.replace(/^```json\s*/, '').replace(/\s*```$/, ''); + } else if (cleanContent.startsWith('```')) { + cleanContent = cleanContent.replace(/^```\s*/, '').replace(/\s*```$/, ''); + } + + const result = JSON.parse(cleanContent); return { cardName: result.cardName || '', setName: result.setName || undefined, @@ -107,12 +99,23 @@ Focus on accuracy. If you can't clearly read something, leave it empty rather th game: result.game || 'UNKNOWN' }; } catch (parseError) { - // If JSON parsing fails, treat the whole response as raw text + console.log('JSON parsing failed, trying text extraction:', parseError); + console.log('Raw content:', content); + + // If JSON parsing fails, try to extract data from the raw text + const extractedData = this.extractDataFromRawResponse(content); return { - cardName: this.extractCardNameFromText(content), + cardName: extractedData.cardName || 'Unknown Card', + setName: extractedData.setName, + setCode: extractedData.setCode, + cardType: extractedData.cardType, + rarity: extractedData.rarity, + hp: extractedData.hp, + attacks: extractedData.attacks || [], + abilities: extractedData.abilities || [], rawText: content, confidence: 70, - game: this.detectGameFromText(content) + game: extractedData.game || 'UNKNOWN' }; } @@ -138,6 +141,57 @@ Focus on accuracy. If you can't clearly read something, leave it empty rather th return lines[0] || 'Unknown Card'; } + // Extract data from raw AI response when JSON parsing fails + private extractDataFromRawResponse(text: string): Partial { + const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); + + let cardName = ''; + let setName = ''; + let setCode = ''; + let cardType = ''; + let rarity = ''; + let hp = ''; + let attacks: string[] = []; + let abilities: string[] = []; + + for (const line of lines) { + // Skip markdown and formatting + if (line.startsWith('```') || line.startsWith('#')) continue; + + // Look for key-value patterns + if (line.match(/card\s*name[:\-]?\s*(.+)/i)) { + cardName = line.replace(/card\s*name[:\-]?\s*/i, '').replace(/['"]/g, ''); + } else if (line.match(/name[:\-]?\s*(.+)/i) && !cardName) { + cardName = line.replace(/name[:\-]?\s*/i, '').replace(/['"]/g, ''); + } else if (line.match(/set[:\-]?\s*(.+)/i)) { + setName = line.replace(/set[:\-]?\s*/i, '').replace(/['"]/g, ''); + } else if (line.match(/hp[:\-]?\s*(\d+)/i)) { + hp = line.match(/hp[:\-]?\s*(\d+)/i)?.[1] || ''; + } else if (line.match(/type[:\-]?\s*(.+)/i)) { + cardType = line.replace(/type[:\-]?\s*/i, '').replace(/['"]/g, ''); + } else if (line.match(/rarity[:\-]?\s*(.+)/i)) { + rarity = line.replace(/rarity[:\-]?\s*/i, '').replace(/['"]/g, ''); + } + } + + // If no structured data found, use the first substantial line as card name + if (!cardName) { + cardName = this.extractCardNameFromText(text); + } + + return { + cardName: cardName || 'Unknown Card', + setName: setName || undefined, + setCode: setCode || undefined, + cardType: cardType || undefined, + rarity: rarity || undefined, + hp: hp || undefined, + attacks: attacks.length > 0 ? attacks : undefined, + abilities: abilities.length > 0 ? abilities : undefined, + game: this.detectGameFromText(text) + }; + } + // Detect game type from text private detectGameFromText(text: string): 'MTG' | 'POKEMON' | 'LORCANA' | 'UNKNOWN' { const lowerText = text.toLowerCase();