← back to Wine Finder
services/wineLabelVerifier.js
365 lines
const axios = require('axios');
const FormData = require('form-data');
/**
* Wine Label Verification Service
*
* Uses Roboflow API for wine label detection and verification
* Falls back to local database matching if API unavailable
*/
class WineLabelVerifier {
constructor() {
// Roboflow API configuration
this.roboflowApiKey = process.env.ROBOFLOW_API_KEY || null;
this.roboflowModelUrl = 'https://detect.roboflow.com/wine-label-detection/1';
// Confidence thresholds
this.confidenceThresholds = {
high: 0.80, // Definitely authentic
medium: 0.60, // Likely authentic
low: 0.40 // Unknown/suspicious
};
// Cache for verification results
this.verificationCache = new Map();
}
/**
* Main verification method - accepts image and returns authentication result
*/
async verifyWineLabel(imageData, options = {}) {
try {
const {
useCache = true,
confidenceThreshold = 0.40,
crossCheckDatabase = true
} = options;
// Check cache first
if (useCache) {
const cached = this.getCachedResult(imageData);
if (cached) {
return { ...cached, fromCache: true };
}
}
// Step 1: Detect label using Roboflow (if API key available)
let roboflowResult = null;
if (this.roboflowApiKey) {
roboflowResult = await this.detectWithRoboflow(imageData, confidenceThreshold);
}
// Step 2: Extract text using basic pattern matching (fallback)
const extractedText = this.extractTextFromPredictions(roboflowResult);
// Step 3: Parse wine information
const wineInfo = this.parseWineInfo(extractedText, roboflowResult);
// Step 4: Cross-check with database (if enabled)
let databaseMatch = null;
if (crossCheckDatabase && wineInfo.name) {
databaseMatch = await this.checkDatabase(wineInfo);
}
// Step 5: Calculate confidence score
const confidence = this.calculateConfidence(roboflowResult, databaseMatch, wineInfo);
// Step 6: Determine verification status
const status = this.determineStatus(confidence);
// Build result
const result = {
success: true,
verification: {
status,
confidence,
wine: wineInfo,
details: {
labelDetected: roboflowResult ? true : false,
apiUsed: this.roboflowApiKey ? 'Roboflow' : 'Local',
textExtracted: extractedText,
databaseMatch: databaseMatch ? true : false,
matchedWines: databaseMatch?.wines || [],
predictions: roboflowResult?.predictions || []
}
},
timestamp: new Date().toISOString()
};
// Cache result
if (useCache) {
this.cacheResult(imageData, result);
}
return result;
} catch (error) {
console.error('Wine label verification error:', error.message);
return {
success: false,
error: error.message,
verification: {
status: 'error',
confidence: 0,
wine: {},
details: {
error: error.message
}
}
};
}
}
/**
* Detect wine label using Roboflow API
*/
async detectWithRoboflow(imageData, confidence = 40) {
if (!this.roboflowApiKey) {
console.warn('Roboflow API key not configured');
return null;
}
try {
// Convert base64 to buffer if needed
let imageBuffer;
if (imageData.startsWith('data:image')) {
// Remove data URL prefix
const base64Data = imageData.replace(/^data:image\/\w+;base64,/, '');
imageBuffer = Buffer.from(base64Data, 'base64');
} else if (Buffer.isBuffer(imageData)) {
imageBuffer = imageData;
} else {
imageBuffer = Buffer.from(imageData, 'base64');
}
// Call Roboflow API
const response = await axios({
method: 'POST',
url: `${this.roboflowModelUrl}`,
params: {
api_key: this.roboflowApiKey,
confidence: confidence
},
data: imageBuffer,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 30000
});
return response.data;
} catch (error) {
console.error('Roboflow API error:', error.message);
return null;
}
}
/**
* Extract text from Roboflow predictions
*/
extractTextFromPredictions(roboflowResult) {
if (!roboflowResult || !roboflowResult.predictions) {
return [];
}
const extractedText = [];
roboflowResult.predictions.forEach(prediction => {
if (prediction.class && prediction.confidence > 0.5) {
extractedText.push(prediction.class);
}
});
return extractedText;
}
/**
* Parse wine information from extracted text and predictions
*/
parseWineInfo(textArray, roboflowResult) {
const info = {
name: '',
vintage: '',
winery: '',
region: '',
appellation: '',
type: '',
alcohol: '',
organic: false,
sustainable: false
};
// Parse from Roboflow predictions if available
if (roboflowResult && roboflowResult.predictions) {
roboflowResult.predictions.forEach(pred => {
const className = pred.class.toLowerCase();
// Extract vintage (year)
const yearMatch = className.match(/\b(19|20)\d{2}\b/);
if (yearMatch && !info.vintage) {
info.vintage = yearMatch[0];
}
// Detect wine type
if (className.includes('cabernet') || className.includes('merlot') ||
className.includes('pinot') || className.includes('chardonnay')) {
info.type = className;
}
// Detect certifications
if (className.includes('organic')) {
info.organic = true;
}
if (className.includes('sustainable')) {
info.sustainable = true;
}
// Extract appellation
if (className.includes('aoc') || className.includes('aop') ||
className.includes('doc') || className.includes('ava')) {
info.appellation = className;
}
});
}
// Parse from text array
textArray.forEach(text => {
const textLower = text.toLowerCase();
// Try to find winery/wine name (usually longest text)
if (text.length > info.name.length && !textLower.match(/\b(19|20)\d{2}\b/)) {
info.name = text;
}
// Extract alcohol percentage
const alcoholMatch = text.match(/(\d+\.?\d*)\s*%/);
if (alcoholMatch) {
info.alcohol = alcoholMatch[1] + '%';
}
});
return info;
}
/**
* Check wine against existing databases
*/
async checkDatabase(wineInfo) {
// This would query your existing wine databases
// For now, return mock structure
try {
// TODO: Implement actual database search
// const results = await wineAggregator.searchAll(wineInfo.name);
return {
found: false,
wines: [],
sources: []
};
} catch (error) {
console.error('Database check error:', error.message);
return null;
}
}
/**
* Calculate overall confidence score
*/
calculateConfidence(roboflowResult, databaseMatch, wineInfo) {
let confidence = 0;
// Roboflow detection confidence (40% weight)
if (roboflowResult && roboflowResult.predictions && roboflowResult.predictions.length > 0) {
const avgConfidence = roboflowResult.predictions.reduce((sum, p) => sum + p.confidence, 0) / roboflowResult.predictions.length;
confidence += avgConfidence * 0.4;
}
// Database match (40% weight)
if (databaseMatch && databaseMatch.found) {
confidence += 0.4;
}
// Wine info completeness (20% weight)
const infoFields = [wineInfo.name, wineInfo.vintage, wineInfo.winery, wineInfo.region];
const filledFields = infoFields.filter(f => f && f.length > 0).length;
confidence += (filledFields / infoFields.length) * 0.2;
return Math.min(confidence, 1.0);
}
/**
* Determine verification status based on confidence
*/
determineStatus(confidence) {
if (confidence >= this.confidenceThresholds.high) {
return 'authentic';
} else if (confidence >= this.confidenceThresholds.medium) {
return 'likely_authentic';
} else if (confidence >= this.confidenceThresholds.low) {
return 'unknown';
} else {
return 'suspicious';
}
}
/**
* Cache verification result
*/
cacheResult(imageData, result) {
// Create simple hash of image data
const hash = this.hashImage(imageData);
this.verificationCache.set(hash, {
result,
timestamp: Date.now()
});
// Limit cache size
if (this.verificationCache.size > 100) {
const firstKey = this.verificationCache.keys().next().value;
this.verificationCache.delete(firstKey);
}
}
/**
* Get cached verification result
*/
getCachedResult(imageData) {
const hash = this.hashImage(imageData);
const cached = this.verificationCache.get(hash);
if (cached) {
// Check if cache is still valid (1 hour)
const age = Date.now() - cached.timestamp;
if (age < 3600000) {
return cached.result.verification;
} else {
this.verificationCache.delete(hash);
}
}
return null;
}
/**
* Simple image hash for caching
*/
hashImage(imageData) {
const crypto = require('crypto');
return crypto.createHash('md5').update(imageData.substring(0, 1000)).digest('hex');
}
/**
* Get service statistics
*/
getStats() {
return {
apiConfigured: this.roboflowApiKey ? true : false,
cacheSize: this.verificationCache.size,
confidenceThresholds: this.confidenceThresholds
};
}
}
module.exports = new WineLabelVerifier();