Fix AI OCR JSON parsing issues

🐛 JSON Parsing Fixes:
- Handle markdown code blocks in AI responses
- Add robust text extraction fallback when JSON parsing fails
- Improve AI prompt to request plain JSON without formatting
- Add extractDataFromRawResponse method for text parsing
- Better error handling and logging for debugging

This should fix the issue where AI returns markdown formatted responses
instead of proper card data extraction.
This commit is contained in:
Randall Stillwell 2025-07-22 10:39:35 -05:00
parent 0bc3e854fb
commit e86782335e

View file

@ -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<CardOCRResult> {
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();