feat: improve AI resilience, add background enrichment loading states, and fix duplicate identifier in TagSelector

This commit is contained in:
2025-12-23 11:38:16 +01:00
parent 1d98bb9947
commit c134c78a2c
37 changed files with 1906 additions and 786 deletions

View File

@@ -1,46 +1,68 @@
export const getSystemPrompt = (availableTags: string, language: string) => `
TASK: Analyze this whisky bottle image. Return raw JSON.
export const getOcrPrompt = () => `
ROLE: High-Precision OCR Engine for Whisky Labels.
OBJECTIVE: Extract visible metadata strictly from the image.
SPEED PRIORITY: Do NOT analyze flavor. Do NOT provide descriptions. Do NOT add tags.
STEP 1: IDENTIFICATION (OCR & EXTRACTION)
Extract exact text and details from the label. Look closely for specific dates and codes.
- name: Full whisky name (e.g. "Lagavulin 16 Year Old")
- distillery: Distillery name
- bottler: Independent bottler if applicable
- category: Type (e.g. "Islay Single Malt", "Bourbon")
- abv: Alcohol percentage (number only)
- age: Age statement in years (number only)
- vintage: Vintage year (e.g. "1995")
- distilled_at: Distillation date/year if specified
- bottled_at: Bottling date/year if specified
- batch_info: Cask number, Batch ID, or Bottle number (e.g. "Batch 001", "Cask #402")
- bottleCode: Laser codes etched on glass/label (e.g. "L1234...")
- whiskybaseId: Whiskybase ID if clearly printed (rare, but check)
TASK:
1. Identify if the image contains a whisky/spirit bottle.
2. Extract the following technical details into the JSON schema below.
3. If a value is not visible or cannot be inferred with high certainty, use null.
STEP 2: SENSORY "MAGIC" (KNOWLEDGE RETRIEVAL)
Use the IDENTIFIED NAME from Step 1 to query your internal knowledge base for the flavor profile.
DO NOT try to "see" the flavor in the pixels. Use your expert knowledge about this specific whisky edition.
- Match flavors strictly against this list: ${availableTags}
- Select top 5-8 matching tags.
- If distinct notes are missing from the list, add 1-2 unique ones to "suggested_custom_tags" (localized in ${language === 'de' ? 'German' : 'English'}).
EXTRACTION RULES:
- Name: Combine Distillery + Age + Edition + Vintage (e.g., "Signatory Vintage Ben Nevis 2019 4 Year Old").
- Distillery: The producer of the spirit.
- Bottler: Independent bottler (e.g., "Signatory", "Gordon & MacPhail") if applicable.
- Batch Info: Capture ALL Cask numbers, Batch IDs, Bottle numbers, Cask Types (e.g., "Refill Oloroso Sherry Butt, Bottle 1135").
- Codes: Look for laser codes etched on glass/label (e.g., "L20394...").
- Dates: Distinguish clearly between Vintage (distilled year), Bottled year, and Age.
OUTPUT SCHEMA (Strict JSON):
{
"name": "string",
"distillery": "string",
"category": "string",
"abv": number or null,
"age": number or null,
"vintage": "string or null",
"distilled_at": "string or null",
"bottled_at": "string or null",
"batch_info": "string or null",
"bottleCode": "string or null",
"whiskybaseId": "string or null",
"bottler": "stringOrNull",
"category": "string (e.g. Single Malt Scotch Whisky)",
"abv": numberOrNull,
"age": numberOrNull,
"vintage": "stringOrNull",
"distilled_at": "stringOrNull (Year/Date)",
"bottled_at": "stringOrNull (Year/Date)",
"batch_info": "stringOrNull",
"bottleCode": "stringOrNull",
"whiskybaseId": "stringOrNull",
"is_whisky": boolean,
"confidence": number,
"suggested_tags": ["tag1", "tag2"],
"suggested_custom_tags": ["custom1"],
"search_string": "site:whiskybase.com [Distillery] [Name] [Vintage/Age]"
"confidence": number
}
`;
export const getEnrichmentPrompt = (name: string, distillery: string, availableTags: string, language: string) => `
TASK: You are a Whisky Sommelier.
INPUT: A whisky named "${name}" from distillery "${distillery}".
1. DATABASE LOOKUP:
Retrieve the sensory profile and specific Whiskybase search string for this bottling.
Use your expert knowledge.
2. TAGGING:
Select the top 5-8 flavor tags strictly from this list:
[${availableTags}]
3. SEARCH STRING:
Create a precise search string for Whiskybase using: "site:whiskybase.com [Distillery] [Vintage/Age] [Bottler/Edition]"
OUTPUT JSON:
{
"suggested_tags": ["tag1", "tag2", "tag3"],
"suggested_custom_tags": ["uniquer_note_if_missing_in_list"],
"search_string": "string"
}
`;
// Legacy support (to avoid immediate breaking changes while refactoring)
export const getSystemPrompt = (availableTags: string, language: string) => `
${getOcrPrompt()}
Additionally, provide:
- suggested_tags: string[] (matched against [${availableTags}])
- suggested_custom_tags: string[]
- search_string: string
`;

View File

@@ -7,6 +7,10 @@ export interface PendingScan {
timestamp: number;
provider?: 'gemini' | 'mistral';
locale?: string;
metadata?: any; // Bottle metadata for offline scans
syncing?: number; // 0 or 1 for indexing
attempts?: number;
last_error?: string;
}
export interface PendingTasting {
@@ -25,6 +29,9 @@ export interface PendingTasting {
};
photo?: string;
tasted_at: string;
syncing?: number; // 0 or 1 for indexing
attempts?: number;
last_error?: string;
}
export interface CachedTag {
@@ -80,9 +87,9 @@ export class WhiskyDexie extends Dexie {
constructor() {
super('WhiskyVault');
this.version(4).stores({
pending_scans: '++id, temp_id, timestamp, locale',
pending_tastings: '++id, bottle_id, pending_bottle_id, tasted_at',
this.version(6).stores({
pending_scans: '++id, temp_id, timestamp, locale, syncing, attempts',
pending_tastings: '++id, bottle_id, pending_bottle_id, tasted_at, syncing, attempts',
cache_tags: 'id, category, name',
cache_buddies: 'id, name',
cache_bottles: 'id, name, distillery',

View File

@@ -5,7 +5,6 @@ const apiKey = process.env.GEMINI_API_KEY!;
const genAI = new GoogleGenerativeAI(apiKey);
export const geminiModel = genAI.getGenerativeModel({
//model: 'gemini-3-flash-preview',
model: 'gemini-2.5-flash',
generationConfig: {
responseMimeType: 'application/json',