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
Showing only changes of commit 7208d1403b - Show all commits

View file

@ -361,7 +361,8 @@ export default function CameraScanner({ onCardScanned, onError }) {
}
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();

View file

@ -212,18 +212,23 @@ export async function logScanAttempt({
resultKind,
latencyMs,
}) {
await sql`
INSERT INTO scan_attempts (
user_id, ocr_text, ocr_confidence, layer,
matched_card_id, result_kind, latency_ms
) VALUES (
${userId},
${ocrText || null},
${ocrConfidence ?? null},
${layer},
${matchedCardId},
${resultKind},
${latencyMs ?? null}
)
`;
try {
await sql`
INSERT INTO scan_attempts (
user_id, ocr_text, ocr_confidence, layer,
matched_card_id, result_kind, latency_ms
) VALUES (
${userId},
${ocrText || null},
${ocrConfidence ?? null},
${layer},
${matchedCardId},
${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 =
'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.).

View file

@ -3,6 +3,51 @@ import { checkScanRateLimit } from '../../../lib/rate-limit.js';
import { analyzeCardImage } from '../../../lib/scan-gemini.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) {
return {
id: card.id,
@ -177,6 +222,7 @@ export default async function handler(req, res) {
});
} catch (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);
}
}