feat: improve AI resilience, add background enrichment loading states, and fix duplicate identifier in TagSelector
This commit is contained in:
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user