fix(scanner): Gemini model + readable scan errors #37

Merged
varutasu merged 1 commit from fix/scan-identify-gemini-errors into main 2026-05-27 13:42:37 -04:00
4 changed files with 70 additions and 17 deletions

View file

@ -361,7 +361,8 @@ export default function CameraScanner({ onCardScanned, onError }) {
} }
if (!response.ok) { if (!response.ok) {
throw new Error(`Scan identify failed: ${response.status}`); const errBody = await response.json().catch(() => ({}));
throw new Error(errBody.error || `Scan identify failed: ${response.status}`);
} }
const result = await response.json(); const result = await response.json();

View file

@ -212,18 +212,23 @@ export async function logScanAttempt({
resultKind, resultKind,
latencyMs, latencyMs,
}) { }) {
await sql` try {
INSERT INTO scan_attempts ( await sql`
user_id, ocr_text, ocr_confidence, layer, INSERT INTO scan_attempts (
matched_card_id, result_kind, latency_ms user_id, ocr_text, ocr_confidence, layer,
) VALUES ( matched_card_id, result_kind, latency_ms
${userId}, ) VALUES (
${ocrText || null}, ${userId},
${ocrConfidence ?? null}, ${ocrText || null},
${layer}, ${ocrConfidence ?? null},
${matchedCardId}, ${layer},
${resultKind}, ${matchedCardId},
${latencyMs ?? null} ${resultKind},
) ${latencyMs ?? null}
`; )
`;
} catch (error) {
// Telemetry must not block identification (e.g. migration not yet applied).
console.error('[logScanAttempt]', error.message);
}
} }

View file

@ -1,5 +1,6 @@
const DEFAULT_VISION_MODEL = process.env.GEMINI_VISION_MODEL || 'gemini-2.5-flash';
const GEMINI_MODEL = const GEMINI_MODEL =
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'; `https://generativelanguage.googleapis.com/v1beta/models/${DEFAULT_VISION_MODEL}:generateContent`;
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.). 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.).

View file

@ -3,6 +3,51 @@ import { checkScanRateLimit } from '../../../lib/rate-limit.js';
import { analyzeCardImage } from '../../../lib/scan-gemini.js'; import { analyzeCardImage } from '../../../lib/scan-gemini.js';
import { matchCardInCatalog, logScanAttempt } from '../../../lib/card-catalog-match.js'; import { matchCardInCatalog, logScanAttempt } from '../../../lib/card-catalog-match.js';
function scanErrorResponse(error) {
const msg = error?.message || '';
if (msg.includes('GEMINI_AI_API_KEY is not configured')) {
return {
status: 503,
body: {
error: 'Card scanning is not configured on this server (missing GEMINI_AI_API_KEY).',
},
};
}
if (msg.includes('Gemini API error: 429')) {
return {
status: 502,
body: {
error: 'Vision service quota exceeded. Check Gemini API billing or try again later.',
},
};
}
if (msg.includes('Gemini API error: 403') || msg.includes('PERMISSION_DENIED')) {
return {
status: 502,
body: {
error: 'Vision service access denied. Regenerate the Gemini API key in Google AI Studio.',
},
};
}
if (msg.includes('scan_attempts') || msg.includes('card_submissions')) {
return {
status: 503,
body: {
error: 'Scan database tables are missing. Run npm run migrate up on the deployment database.',
},
};
}
return {
status: 500,
body: { error: 'Internal server error' },
};
}
function formatCardResponse(card, ocrResult) { function formatCardResponse(card, ocrResult) {
return { return {
id: card.id, id: card.id,
@ -177,6 +222,7 @@ export default async function handler(req, res) {
}); });
} catch (error) { } catch (error) {
console.error('[POST /api/scan/identify]', error); console.error('[POST /api/scan/identify]', error);
return res.status(500).json({ error: 'Internal server error' }); const { status, body } = scanErrorResponse(error);
return res.status(status).json(body);
} }
} }