import { StateSetClient } from 'stateset-node';
import FormData from 'form-data';
import fs from 'fs';
import { logger } from './logger';
class VisionService {
private client: StateSetClient;
private cache: Map<string, any> = new Map();
constructor(apiKey: string) {
this.client = new StateSetClient({ apiKey });
}
/**
* Analyze product images with comprehensive error handling
*/
async analyzeProductImage(imagePath: string, productId: string) {
try {
// Check cache first
const cacheKey = `${productId}_${imagePath}`;
if (this.cache.has(cacheKey)) {
logger.info('Returning cached analysis', { productId });
return this.cache.get(cacheKey);
}
// Validate image exists and size
const stats = await fs.promises.stat(imagePath);
if (stats.size > 4 * 1024 * 1024) {
throw new Error('Image size exceeds 4MB limit');
}
// Prepare form data
const formData = new FormData();
formData.append('image', fs.createReadStream(imagePath));
formData.append('analysis_types', 'object_detection,quality_assessment,text_extraction');
// Perform analysis with timeout
const analysis = await this.analyzeWithTimeout(formData, 30000);
// Validate results
if (analysis.quality_assessment.score < 0.7) {
logger.warn('Low quality image detected', {
productId,
qualityScore: analysis.quality_assessment.score
});
}
// Cache successful results
this.cache.set(cacheKey, analysis);
// Clean up old cache entries
if (this.cache.size > 100) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
return analysis;
} catch (error) {
logger.error('Product image analysis failed', { error, productId, imagePath });
// Return graceful degradation response
return {
success: false,
error: this.formatError(error),
fallback: {
productId,
requiresManualReview: true,
timestamp: new Date().toISOString()
}
};
}
}
/**
* Batch process return images with progress tracking
*/
async processReturnImages(returnId: string, images: string[], onProgress?: (progress: number) => void) {
const results = [];
const total = images.length;
for (let i = 0; i < images.length; i++) {
try {
const result = await this.assessDamage(images[i]);
results.push({
imageIndex: i,
imagePath: images[i],
...result
});
// Report progress
if (onProgress) {
onProgress(((i + 1) / total) * 100);
}
} catch (error) {
logger.error('Failed to process return image', {
returnId,
imageIndex: i,
error
});
results.push({
imageIndex: i,
imagePath: images[i],
error: error.message,
requiresManualReview: true
});
}
}
// Calculate overall condition
const overallCondition = this.calculateReturnCondition(results);
// Update return record
try {
await this.client.returns.update({
id: returnId,
condition_assessment: results,
overall_condition: overallCondition,
assessed_at: new Date().toISOString()
});
} catch (error) {
logger.error('Failed to update return record', { returnId, error });
}
return {
returnId,
images: results,
overallCondition,
summary: this.generateConditionSummary(results)
};
}
private async analyzeWithTimeout(formData: FormData, timeoutMs: number) {
return Promise.race([
this.client.vision.analyze({ formData }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Analysis timeout')), timeoutMs)
)
]);
}
private async assessDamage(imagePath: string) {
const formData = new FormData();
formData.append('image', fs.createReadStream(imagePath));
const analysis = await this.client.vision.analyzeUrl({
url: imagePath,
analysis_types: ['damage_detection', 'wear_assessment']
});
return {
damageDetected: analysis.damage_detection.detected,
damageSeverity: analysis.damage_detection.severity,
damageTypes: analysis.damage_detection.types || [],
wearLevel: analysis.wear_assessment.level,
confidence: analysis.confidence
};
}
private calculateReturnCondition(results: any[]): string {
const validResults = results.filter(r => !r.error);
if (validResults.length === 0) return 'UNKNOWN';
const severities = validResults.map(r => r.damageSeverity);
if (severities.includes('major')) return 'C';
if (severities.includes('minor')) return 'B';
return 'A';
}
private generateConditionSummary(results: any[]): string {
const damageCount = results.filter(r => r.damageDetected).length;
const errorCount = results.filter(r => r.error).length;
if (errorCount === results.length) {
return 'Unable to assess condition - manual review required';
}
if (damageCount === 0) {
return 'Item appears to be in good condition';
}
const damageTypes = [...new Set(results.flatMap(r => r.damageTypes || []))];
return `Damage detected on ${damageCount} of ${results.length} images. Types: ${damageTypes.join(', ')}`;
}
private formatError(error: any): string {
if (error.code === 'INVALID_IMAGE_FORMAT') {
return 'Please upload a valid image file (JPEG, PNG, GIF, or BMP)';
} else if (error.code === 'IMAGE_TOO_LARGE') {
return 'Image must be less than 4MB';
} else if (error.code === 'RATE_LIMIT_EXCEEDED') {
return 'Too many requests. Please try again in a few moments';
} else if (error.message === 'Analysis timeout') {
return 'Image analysis is taking longer than expected. Please try again';
}
return 'An unexpected error occurred. Please try again';
}
}
// Usage example
async function main() {
const visionService = new VisionService(process.env.STATESET_API_KEY!);
// Process a product image
const productAnalysis = await visionService.analyzeProductImage(
'./product-images/shoe-1.jpg',
'PROD-12345'
);
if (productAnalysis.success !== false) {
logger.info('Product analysis complete', {
productId: 'PROD-12345',
quality: productAnalysis.quality_assessment
});
}
// Process return images with progress tracking
const returnImages = [
'./returns/return-001-front.jpg',
'./returns/return-001-back.jpg',
'./returns/return-001-detail.jpg'
];
const returnAssessment = await visionService.processReturnImages(
'RET-78901',
returnImages,
(progress) => {
logger.info(`Processing return images: ${progress}% complete`);
}
);
logger.info('Return assessment complete', {
returnId: returnAssessment.returnId,
condition: returnAssessment.overallCondition,
summary: returnAssessment.summary
});
}
// Run the example
main().catch(console.error);