fix(scanner): gemini-2.5-flash model + actionable scan error messages
Use the same vision model as the deleted browser client, surface Gemini quota/denial/migration failures as 502/503 with readable text, and stop scan_attempts telemetry from blocking identification. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ecb3ee12fc
commit
7208d1403b
4 changed files with 70 additions and 17 deletions
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ export async function logScanAttempt({
|
||||||
resultKind,
|
resultKind,
|
||||||
latencyMs,
|
latencyMs,
|
||||||
}) {
|
}) {
|
||||||
|
try {
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO scan_attempts (
|
INSERT INTO scan_attempts (
|
||||||
user_id, ocr_text, ocr_confidence, layer,
|
user_id, ocr_text, ocr_confidence, layer,
|
||||||
|
|
@ -226,4 +227,8 @@ export async function logScanAttempt({
|
||||||
${latencyMs ?? null}
|
${latencyMs ?? null}
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
// Telemetry must not block identification (e.g. migration not yet applied).
|
||||||
|
console.error('[logScanAttempt]', error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue