feat: Replace Nebius with Pixtral AI for bottle scanning
This commit is contained in:
@@ -1,22 +1,20 @@
|
||||
'use server';
|
||||
|
||||
import { getNebiusClient } from '@/lib/ai-client';
|
||||
import { SYSTEM_INSTRUCTION as GEMINI_SYSTEM_INSTRUCTION } from '@/lib/gemini';
|
||||
import { Mistral } from '@mistralai/mistralai';
|
||||
import { BottleMetadataSchema, AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { createHash } from 'crypto';
|
||||
import { trackApiUsage } from './track-api-usage';
|
||||
import { checkCreditBalance, deductCredits } from './credit-service';
|
||||
|
||||
export async function analyzeBottleNebius(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
if (!process.env.NEBIUS_API_KEY) {
|
||||
return { success: false, error: 'NEBIUS_API_KEY is not configured.' };
|
||||
export async function analyzeBottlePixtral(base64Image: string, tags?: string[], locale: string = 'de'): Promise<AnalysisResponse & { search_string?: string }> {
|
||||
if (!process.env.MISTRAL_API_KEY) {
|
||||
return { success: false, error: 'MISTRAL_API_KEY is not configured.' };
|
||||
}
|
||||
|
||||
let supabase;
|
||||
try {
|
||||
supabase = await createClient();
|
||||
console.log('[analyzeBottleNebius] Initialized Supabase client');
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session || !session.user) {
|
||||
return { success: false, error: 'Nicht autorisiert oder Session abgelaufen.' };
|
||||
@@ -48,63 +46,70 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[],
|
||||
};
|
||||
}
|
||||
|
||||
const instruction = GEMINI_SYSTEM_INSTRUCTION
|
||||
.replace('{AVAILABLE_TAGS}', tags ? tags.join(', ') : 'No tags available')
|
||||
.replace('{LANGUAGE}', locale === 'en' ? 'English' : 'German')
|
||||
+ "\nAdditionally, generate a 'search_string' field for Whiskybase in this format: 'site:whiskybase.com [Distillery] [Name] [Vintage]'. Include this field in the JSON object.";
|
||||
console.log(`[analyzeBottleNebius] Instruction prepared for AI: ${instruction.substring(0, 100)}...`);
|
||||
const client = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
|
||||
const dataUrl = `data:image/jpeg;base64,${base64Data}`;
|
||||
|
||||
const prompt = `Du bist ein Whisky-Experte und OCR-Spezialist.
|
||||
Analysiere dieses Etikett präzise.
|
||||
Sprache für Beschreibungen: ${locale === 'en' ? 'Englisch' : 'Deutsch'}.
|
||||
Verfügbare Tags zur Einordnung: ${tags ? tags.join(', ') : 'Keine Tags verfügbar'}.
|
||||
|
||||
const client = getNebiusClient();
|
||||
const response = await client.chat.completions.create({
|
||||
model: "Qwen/Qwen2.5-VL-72B-Instruct",
|
||||
Antworte AUSSCHLIESSLICH mit gültigem JSON (kein Markdown, kein Text davor/danach):
|
||||
{
|
||||
"distillery": "Name der Brennerei (z.B. Lagavulin)",
|
||||
"name": "Exakter Name/Edition (z.B. 16 Year Old)",
|
||||
"vintage": "Jahrgang als Zahl oder null",
|
||||
"age": "Alter als Zahl oder null (z.B. 16)",
|
||||
"abv": "Alkoholgehalt als Zahl ohne % (z.B. 43)",
|
||||
"category": "Kategorie (z.B. Single Malt Scotch Whisky)",
|
||||
"search_string": "site:whiskybase.com [Brennerei] [Name] [Alter]"
|
||||
}`;
|
||||
|
||||
const chatResponse = await client.chat.complete({
|
||||
model: 'pixtral-large-latest',
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: instruction
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Extract whisky metadata from this image."
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: `data:image/jpeg;base64,${base64Data}`
|
||||
}
|
||||
}
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', imageUrl: dataUrl }
|
||||
]
|
||||
}
|
||||
],
|
||||
response_format: { type: "json_object" }
|
||||
responseFormat: { type: 'json_object' },
|
||||
temperature: 0.1
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error('Empty response from Nebius AI');
|
||||
const rawContent = chatResponse.choices?.[0].message.content;
|
||||
if (!rawContent) throw new Error("Keine Antwort von Pixtral");
|
||||
|
||||
// Extract JSON content in case the model wraps it in markdown blocks
|
||||
const jsonContent = content.match(/\{[\s\S]*\}/)?.[0] || content;
|
||||
const jsonData = JSON.parse(jsonContent);
|
||||
const jsonData = JSON.parse(rawContent as string);
|
||||
|
||||
// Extract search_string before validation if it's not in schema
|
||||
// Extract search_string before validation
|
||||
const searchString = jsonData.search_string;
|
||||
delete jsonData.search_string;
|
||||
|
||||
// Ensure abv is a number if it came as a string
|
||||
if (typeof jsonData.abv === 'string') {
|
||||
jsonData.abv = parseFloat(jsonData.abv.replace('%', '').trim());
|
||||
}
|
||||
|
||||
// Ensure age/vintage are numbers
|
||||
if (jsonData.age) jsonData.age = parseInt(jsonData.age);
|
||||
if (jsonData.vintage) jsonData.vintage = parseInt(jsonData.vintage);
|
||||
|
||||
const validatedData = BottleMetadataSchema.parse(jsonData);
|
||||
|
||||
// Track usage
|
||||
await trackApiUsage({
|
||||
userId: userId,
|
||||
apiType: 'gemini_ai', // Keep tracking as gemini_ai for budget or separate later
|
||||
endpoint: 'nebius/qwen2.5-vl',
|
||||
apiType: 'gemini_ai', // Keep as generic 'gemini_ai' for now or update schema later
|
||||
endpoint: 'mistral/pixtral-large',
|
||||
success: true
|
||||
});
|
||||
|
||||
// Deduct credits
|
||||
await deductCredits(userId, 'gemini_ai', 'Nebius AI analysis');
|
||||
await deductCredits(userId, 'gemini_ai', 'Pixtral AI analysis');
|
||||
|
||||
// Store in Cache
|
||||
await supabase
|
||||
@@ -118,7 +123,7 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Nebius Analysis Error:', error);
|
||||
console.error('Pixtral Analysis Error:', error);
|
||||
|
||||
// Track failed API call
|
||||
try {
|
||||
@@ -128,7 +133,7 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[],
|
||||
await trackApiUsage({
|
||||
userId: session.user.id,
|
||||
apiType: 'gemini_ai',
|
||||
endpoint: 'nebius/qwen2.5-vl',
|
||||
endpoint: 'mistral/pixtral-large',
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
@@ -140,7 +145,7 @@ export async function analyzeBottleNebius(base64Image: string, tags?: string[],
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Nebius AI analysis failed.',
|
||||
error: error instanceof Error ? error.message : 'Pixtral AI analysis failed.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
'use server';
|
||||
|
||||
import { analyzeBottle } from './analyze-bottle';
|
||||
import { analyzeBottleNebius } from './analyze-bottle-nebius';
|
||||
import { analyzeBottlePixtral } from './analyze-bottle-pixtral';
|
||||
import { searchBraveForWhiskybase } from './brave-search';
|
||||
import { getAllSystemTags } from './tags';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { AnalysisResponse, BottleMetadata } from '@/types/whisky';
|
||||
|
||||
export async function magicScan(base64Image: string, provider: 'gemini' | 'nebius' = 'gemini', locale: string = 'de'): Promise<AnalysisResponse & { wb_id?: string }> {
|
||||
export async function magicScan(base64Image: string, provider: 'gemini' | 'pixtral' = 'gemini', locale: string = 'de'): Promise<AnalysisResponse & { wb_id?: string }> {
|
||||
try {
|
||||
console.log('[magicScan] Starting scan process...');
|
||||
if (!supabase) {
|
||||
@@ -22,8 +22,8 @@ export async function magicScan(base64Image: string, provider: 'gemini' | 'nebiu
|
||||
|
||||
// 1. AI Analysis
|
||||
let aiResponse: any;
|
||||
if (provider === 'nebius') {
|
||||
aiResponse = await analyzeBottleNebius(base64Image, tagNames, locale);
|
||||
if (provider === 'pixtral') {
|
||||
aiResponse = await analyzeBottlePixtral(base64Image, tagNames, locale);
|
||||
} else {
|
||||
aiResponse = await analyzeBottle(base64Image, tagNames, locale);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user