changes: Improve detecction & identifications
This commit is contained in:
36
app/page.tsx
36
app/page.tsx
@@ -30,6 +30,7 @@ function HomePageContent() {
|
|||||||
const [detectedSKU, setDetectedSKU] = useState<string | null>(null);
|
const [detectedSKU, setDetectedSKU] = useState<string | null>(null);
|
||||||
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
|
const [notFoundMessage, setNotFoundMessage] = useState(false);
|
||||||
|
|
||||||
// Effect to clean up the stream when component unmounts or stream changes
|
// Effect to clean up the stream when component unmounts or stream changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -127,6 +128,7 @@ function HomePageContent() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
|
setNotFoundMessage(false); // Limpiar mensaje anterior
|
||||||
console.log('📸 Capturando imagen del video...');
|
console.log('📸 Capturando imagen del video...');
|
||||||
|
|
||||||
// Capture frame from video
|
// Capture frame from video
|
||||||
@@ -145,10 +147,10 @@ function HomePageContent() {
|
|||||||
const sku = await skuIdentificationService.identifySKU(imageData);
|
const sku = await skuIdentificationService.identifySKU(imageData);
|
||||||
console.log('📦 SKU result:', sku);
|
console.log('📦 SKU result:', sku);
|
||||||
|
|
||||||
|
if (sku) {
|
||||||
|
// SKU encontrado - guardar y abrir modal
|
||||||
setDetectedSKU(sku);
|
setDetectedSKU(sku);
|
||||||
|
|
||||||
if (sku) {
|
|
||||||
// Create shoe object with SKU for history
|
|
||||||
const shoeWithSKU: Shoe = {
|
const shoeWithSKU: Shoe = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
name: `Producto ${sku}`,
|
name: `Producto ${sku}`,
|
||||||
@@ -161,14 +163,30 @@ function HomePageContent() {
|
|||||||
|
|
||||||
const updatedHistory = addToHistory(shoeWithSKU);
|
const updatedHistory = addToHistory(shoeWithSKU);
|
||||||
setHistory(updatedHistory);
|
setHistory(updatedHistory);
|
||||||
}
|
|
||||||
|
|
||||||
|
// SOLO abrir popup si hay SKU
|
||||||
setPopupOpen(true);
|
setPopupOpen(true);
|
||||||
|
} else {
|
||||||
|
// NO encontrado - mostrar mensaje temporal
|
||||||
|
console.log('❌ No se encontró zapato');
|
||||||
|
setDetectedSKU(null);
|
||||||
|
setNotFoundMessage(true);
|
||||||
|
|
||||||
|
// Ocultar mensaje después de 3 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotFoundMessage(false);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error en identificación:', error);
|
console.error('❌ Error en identificación:', error);
|
||||||
setDetectedSKU(null);
|
setDetectedSKU(null);
|
||||||
setPopupOpen(true);
|
setNotFoundMessage(true);
|
||||||
|
|
||||||
|
// Ocultar mensaje después de 3 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotFoundMessage(false);
|
||||||
|
}, 3000);
|
||||||
} finally {
|
} finally {
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
}
|
}
|
||||||
@@ -352,6 +370,16 @@ function HomePageContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Not Found Message */}
|
||||||
|
{notFoundMessage && (
|
||||||
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-red-600/90 backdrop-blur-sm rounded-lg px-6 py-3 text-white font-medium shadow-lg animate-in fade-in slide-in-from-top-4 duration-300">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xl">❌</span>
|
||||||
|
<span>Zapato no encontrado</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Settings Panel */}
|
{/* Settings Panel */}
|
||||||
<div className={`absolute left-0 top-0 bottom-0 w-80 bg-black/80 backdrop-blur-xl border-r border-white/20 transform transition-transform duration-500 ease-out z-40 ${
|
<div className={`absolute left-0 top-0 bottom-0 w-80 bg-black/80 backdrop-blur-xl border-r border-white/20 transform transition-transform duration-500 ease-out z-40 ${
|
||||||
isSettingsPanelOpen ? 'translate-x-0' : '-translate-x-full'
|
isSettingsPanelOpen ? 'translate-x-0' : '-translate-x-full'
|
||||||
|
|||||||
@@ -3,83 +3,62 @@ interface SKUResponse {
|
|||||||
SKU: string | null;
|
SKU: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CacheEntry {
|
|
||||||
response: SKUResponse;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for identifying product SKUs from shoe images using external API
|
* Service for identifying product SKUs from shoe images using external API
|
||||||
*/
|
*/
|
||||||
export class SKUIdentificationService {
|
export class SKUIdentificationService {
|
||||||
private cache = new Map<string, CacheEntry>();
|
private readonly API_ENDPOINT: string;
|
||||||
private readonly API_ENDPOINT = `${process.env.NEXT_PUBLIC_PYTHON_SERVER_URL}/predictfile`;
|
|
||||||
private readonly CACHE_TTL = 60 * 60 * 1000; // 1 hour cache
|
constructor() {
|
||||||
private readonly MAX_CACHE_SIZE = 100; // Prevent memory leaks
|
// Construir endpoint correctamente, manejando si la URL base termina con / o no
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_PYTHON_SERVER_URL || '';
|
||||||
|
const cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||||
|
this.API_ENDPOINT = `${cleanBaseUrl}/predictfile`;
|
||||||
|
|
||||||
|
console.log('🔧 SKU Service inicializado:');
|
||||||
|
console.log(' Base URL:', baseUrl);
|
||||||
|
console.log(' Endpoint final:', this.API_ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert ImageData to File object for form-data upload
|
* Validate API response format
|
||||||
|
* Expected format: { "status": true, "SKU": "171312" }
|
||||||
*/
|
*/
|
||||||
private imageDataToFile(imageData: ImageData, filename: string = 'shoe.jpg'): File {
|
private validateResponse(data: any): data is SKUResponse {
|
||||||
// Create canvas from ImageData
|
// Check if data is an object
|
||||||
const canvas = document.createElement('canvas');
|
if (!data || typeof data !== 'object') {
|
||||||
const ctx = canvas.getContext('2d')!;
|
console.error('❌ Invalid response: not an object', data);
|
||||||
canvas.width = imageData.width;
|
return false;
|
||||||
canvas.height = imageData.height;
|
|
||||||
|
|
||||||
// Put image data onto canvas
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
// Convert canvas to blob then to File
|
|
||||||
return new Promise<File>((resolve) => {
|
|
||||||
canvas.toBlob((blob) => {
|
|
||||||
if (blob) {
|
|
||||||
const file = new File([blob], filename, { type: 'image/jpeg' });
|
|
||||||
resolve(file);
|
|
||||||
}
|
|
||||||
}, 'image/jpeg', 0.8); // 80% quality for optimal size/quality balance
|
|
||||||
}) as Promise<File>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Check if 'status' field exists
|
||||||
* Generate a simple hash from image data for caching
|
if (!('status' in data)) {
|
||||||
*/
|
console.error('❌ Invalid response: missing "status" field', data);
|
||||||
private generateImageHash(imageData: ImageData): string {
|
return false;
|
||||||
// Sample pixels at regular intervals for hash generation
|
|
||||||
const step = Math.floor(imageData.data.length / 1000); // Sample ~1000 pixels
|
|
||||||
let hash = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < imageData.data.length; i += step) {
|
|
||||||
hash = ((hash << 5) - hash + imageData.data[i]) & 0xffffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hash.toString(36);
|
// Validate status is boolean
|
||||||
|
if (typeof data.status !== 'boolean') {
|
||||||
|
console.error('❌ Invalid response: "status" must be boolean', data);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Check if 'SKU' field exists
|
||||||
* Clean expired cache entries and maintain size limit
|
if (!('SKU' in data)) {
|
||||||
*/
|
console.error('❌ Invalid response: missing "SKU" field', data);
|
||||||
private cleanCache(): void {
|
return false;
|
||||||
const now = Date.now();
|
|
||||||
const entries = Array.from(this.cache.entries());
|
|
||||||
|
|
||||||
// Remove expired entries
|
|
||||||
for (const [key, entry] of entries) {
|
|
||||||
if (now - entry.timestamp > this.CACHE_TTL) {
|
|
||||||
this.cache.delete(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If still over limit, remove oldest entries
|
// Validate SKU is string or null
|
||||||
if (this.cache.size > this.MAX_CACHE_SIZE) {
|
if (data.SKU !== null && typeof data.SKU !== 'string') {
|
||||||
const sortedEntries = Array.from(this.cache.entries())
|
console.error('❌ Invalid response: "SKU" must be string or null', data);
|
||||||
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const entriesToRemove = sortedEntries.slice(0, this.cache.size - this.MAX_CACHE_SIZE);
|
// All validations passed
|
||||||
for (const [key] of entriesToRemove) {
|
console.log('✅ Response format validated successfully');
|
||||||
this.cache.delete(key);
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,17 +68,10 @@ export class SKUIdentificationService {
|
|||||||
*/
|
*/
|
||||||
async identifySKU(imageData: ImageData): Promise<string | null> {
|
async identifySKU(imageData: ImageData): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
// Generate hash for caching
|
console.log('\n🔍 ========== IDENTIFICACIÓN DE SKU ==========');
|
||||||
const imageHash = this.generateImageHash(imageData);
|
console.log('📊 Dimensiones imagen:', imageData.width, 'x', imageData.height);
|
||||||
|
console.log('🌐 Endpoint:', this.API_ENDPOINT);
|
||||||
// Check cache first
|
console.log('📡 Llamando al API Python (sin cache)...');
|
||||||
const cached = this.cache.get(imageHash);
|
|
||||||
if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL) {
|
|
||||||
console.log('🎯 SKU cache hit:', cached.response);
|
|
||||||
return cached.response.status && cached.response.SKU ? cached.response.SKU : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📡 Calling SKU identification API...');
|
|
||||||
|
|
||||||
// Convert ImageData to File synchronously (simplified approach)
|
// Convert ImageData to File synchronously (simplified approach)
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -118,32 +90,12 @@ export class SKUIdentificationService {
|
|||||||
lastModified: Date.now()
|
lastModified: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('📸 Created file:', {
|
console.log('📸 Imagen convertida a archivo:', file.size, 'bytes');
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
lastModified: file.lastModified
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prepare form data
|
// Prepare form data
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
console.log('📤 FormData prepared with file key');
|
|
||||||
|
|
||||||
// Debug FormData contents
|
|
||||||
for (const [key, value] of formData.entries()) {
|
|
||||||
console.log(`📋 FormData entry: ${key} =`, value);
|
|
||||||
if (value instanceof File) {
|
|
||||||
console.log(` 📄 File details: ${value.name}, ${value.size} bytes, ${value.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🚀 Making API call to:', this.API_ENDPOINT);
|
|
||||||
console.log('🔧 Request method: POST');
|
|
||||||
console.log('🔧 Request headers:', { 'ngrok-skip-browser-warning': 'true' });
|
|
||||||
console.log('🔧 Request body type:', typeof formData, formData.constructor.name);
|
|
||||||
|
|
||||||
// Make API call with CORS headers
|
// Make API call with CORS headers
|
||||||
const response = await fetch(this.API_ENDPOINT, {
|
const response = await fetch(this.API_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -156,67 +108,61 @@ export class SKUIdentificationService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('📡 Response status:', response.status, response.statusText);
|
|
||||||
console.log('📡 Response headers:', Object.fromEntries(response.headers.entries()));
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
|
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get response as text first to debug
|
console.log('✓ Respuesta recibida - Status:', response.status);
|
||||||
|
|
||||||
|
// Try to get response as text first
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
console.log('📡 Raw response text:', responseText);
|
|
||||||
|
|
||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
let data: SKUResponse;
|
let data: any;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(responseText);
|
data = JSON.parse(responseText);
|
||||||
console.log('📦 SKU API response parsed:', data);
|
console.log('✓ Respuesta parseada:', data);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.error('❌ Failed to parse JSON response:', parseError);
|
console.error('❌ Error al parsear JSON:', parseError);
|
||||||
console.error('❌ Response was:', responseText);
|
console.error(' Respuesta recibida:', responseText);
|
||||||
throw new Error(`Invalid JSON response: ${parseError}`);
|
throw new Error(`Invalid JSON response: ${parseError}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the response (both success and failure)
|
// Validate response format
|
||||||
this.cleanCache();
|
if (!this.validateResponse(data)) {
|
||||||
this.cache.set(imageHash, {
|
throw new Error('Response format validation failed');
|
||||||
response: data,
|
}
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return SKU only if status is true and SKU is not null
|
// ONLY return SKU if status is EXACTLY true (not false, not null, not undefined)
|
||||||
return data.status && data.SKU ? data.SKU : null;
|
if (data.status === true && data.SKU) {
|
||||||
|
console.log('✅ ZAPATO ENCONTRADO - SKU:', data.SKU);
|
||||||
} catch (error) {
|
console.log('=========================================\n');
|
||||||
console.error('❌ SKU identification failed:', error);
|
return data.SKU;
|
||||||
// Return null on any error (network, parsing, etc.)
|
} else {
|
||||||
|
// Log detailed reason for not finding shoe
|
||||||
|
if (data.status === false) {
|
||||||
|
console.log('❌ ZAPATO NO ENCONTRADO - El servidor reportó status: false');
|
||||||
|
} else if (data.status === null) {
|
||||||
|
console.log('❌ ZAPATO NO ENCONTRADO - El servidor reportó status: null');
|
||||||
|
} else if (!data.SKU) {
|
||||||
|
console.log('❌ ZAPATO NO ENCONTRADO - status: true pero SKU es null/vacío');
|
||||||
|
} else {
|
||||||
|
console.log('❌ ZAPATO NO ENCONTRADO - status:', data.status, 'SKU:', data.SKU);
|
||||||
|
}
|
||||||
|
console.log('=========================================\n');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ ERROR EN IDENTIFICACIÓN:', error);
|
||||||
|
console.error(' Detalles:', {
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
endpoint: this.API_ENDPOINT
|
||||||
|
});
|
||||||
|
console.log('=========================================\n');
|
||||||
|
// Return null on any error (network, parsing, validation, etc.)
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cached results
|
|
||||||
*/
|
|
||||||
clearCache(): void {
|
|
||||||
this.cache.clear();
|
|
||||||
console.log('🗑️ SKU cache cleared');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache statistics for debugging
|
|
||||||
*/
|
|
||||||
getCacheStats() {
|
|
||||||
const now = Date.now();
|
|
||||||
const entries = Array.from(this.cache.values());
|
|
||||||
const validEntries = entries.filter(entry => (now - entry.timestamp) < this.CACHE_TTL);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalEntries: this.cache.size,
|
|
||||||
validEntries: validEntries.length,
|
|
||||||
expiredEntries: this.cache.size - validEntries.length,
|
|
||||||
cacheHitRate: validEntries.length / Math.max(1, this.cache.size)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user