Pyhton server integration
This commit is contained in:
149
app/page.tsx
149
app/page.tsx
@@ -7,13 +7,12 @@ import { Camera, History, VideoOff, Settings, Video } from 'lucide-react';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { SHOE_DATABASE, type Shoe } from '@/lib/shoe-database';
|
import { type Shoe } from '@/lib/shoe-database';
|
||||||
import { detectShoe } from '@/lib/ml-classification';
|
|
||||||
import { addToHistory, getHistory } from '@/lib/history-storage';
|
import { addToHistory, getHistory } from '@/lib/history-storage';
|
||||||
import ShoeResultsPopup from '@/components/shoe-results-popup';
|
import ShoeResultsPopup from '@/components/shoe-results-popup';
|
||||||
import HistorySidebar from '@/components/history-sidebar';
|
import HistorySidebar from '@/components/history-sidebar';
|
||||||
import { useDetection } from '@/lib/ml/use-detection';
|
import { useDetection } from '@/lib/ml/use-detection';
|
||||||
import type { DetectionResult, DetectionConfig } from '@/lib/ml/types';
|
import type { DetectionResult } from '@/lib/ml/types';
|
||||||
|
|
||||||
type CameraStatus = 'loading' | 'active' | 'denied' | 'no_devices';
|
type CameraStatus = 'loading' | 'active' | 'denied' | 'no_devices';
|
||||||
|
|
||||||
@@ -31,6 +30,7 @@ export default function HomePage() {
|
|||||||
const [isPopupOpen, setPopupOpen] = useState(false);
|
const [isPopupOpen, setPopupOpen] = useState(false);
|
||||||
const [isHistoryOpen, setHistoryOpen] = useState(false);
|
const [isHistoryOpen, setHistoryOpen] = useState(false);
|
||||||
const [history, setHistory] = useState<Shoe[]>([]);
|
const [history, setHistory] = useState<Shoe[]>([]);
|
||||||
|
const [detectedSKU, setDetectedSKU] = useState<string | null>(null);
|
||||||
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
|
||||||
|
|
||||||
// ML Detection state
|
// ML Detection state
|
||||||
@@ -40,8 +40,31 @@ export default function HomePage() {
|
|||||||
const [lastSoundTime, setLastSoundTime] = useState(0);
|
const [lastSoundTime, setLastSoundTime] = useState(0);
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize ML detection system first
|
||||||
|
const {
|
||||||
|
isLoading: isMLLoading,
|
||||||
|
metrics,
|
||||||
|
error: mlError,
|
||||||
|
initialize: initializeML,
|
||||||
|
startContinuous,
|
||||||
|
stopContinuous,
|
||||||
|
triggerDetection,
|
||||||
|
updateConfig,
|
||||||
|
config,
|
||||||
|
detectionEngine,
|
||||||
|
setDetectionCallback
|
||||||
|
} = useDetection({
|
||||||
|
modelVariant: 'standard', // Start with standard model
|
||||||
|
enableContinuous: true,
|
||||||
|
enableTrigger: true,
|
||||||
|
onDetection: undefined, // Will be set after handleDetection is defined
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('ML Detection Error:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Clean detection callback - no canvas drawing needed
|
// Clean detection callback - no canvas drawing needed
|
||||||
const handleDetection = useCallback((detection: DetectionResult | null) => {
|
const handleDetection = useCallback(async (detection: DetectionResult | null) => {
|
||||||
const callbackId = Math.random().toString(36).substr(2, 9);
|
const callbackId = Math.random().toString(36).substr(2, 9);
|
||||||
console.log(`🔍 Detection callback received [${callbackId}]:`, detection);
|
console.log(`🔍 Detection callback received [${callbackId}]:`, detection);
|
||||||
setCurrentDetection(detection);
|
setCurrentDetection(detection);
|
||||||
@@ -54,6 +77,37 @@ export default function HomePage() {
|
|||||||
// Auto-trigger popup when shoe is detected with high confidence
|
// Auto-trigger popup when shoe is detected with high confidence
|
||||||
if (detection && detection.confidence > 0.7) {
|
if (detection && detection.confidence > 0.7) {
|
||||||
console.log(`🎯 HIGH CONFIDENCE SHOE DETECTED! [${callbackId}] Opening popup...`, detection);
|
console.log(`🎯 HIGH CONFIDENCE SHOE DETECTED! [${callbackId}] Opening popup...`, detection);
|
||||||
|
|
||||||
|
// Call SKU identification API
|
||||||
|
if (videoRef.current && detectionEngine) {
|
||||||
|
try {
|
||||||
|
console.log(`🔍 [${callbackId}] Calling SKU identification...`);
|
||||||
|
const sku = await detectionEngine.identifyProductSKU(videoRef.current);
|
||||||
|
console.log(`📦 [${callbackId}] SKU result:`, sku);
|
||||||
|
setDetectedSKU(sku);
|
||||||
|
|
||||||
|
if (sku) {
|
||||||
|
// Create shoe object with SKU for history
|
||||||
|
const shoeWithSKU: Shoe = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: `Producto ${sku}`,
|
||||||
|
brand: 'Identificado por IA',
|
||||||
|
price: 'Precio por consultar',
|
||||||
|
image: '/placeholder.jpg',
|
||||||
|
confidence: detection.confidence,
|
||||||
|
sku: sku,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedHistory = addToHistory(shoeWithSKU);
|
||||||
|
setHistory(updatedHistory);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [${callbackId}] SKU identification failed:`, error);
|
||||||
|
setDetectedSKU(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setPopupOpen(true);
|
setPopupOpen(true);
|
||||||
|
|
||||||
// Play detection sound with debouncing (max once per 2 seconds)
|
// Play detection sound with debouncing (max once per 2 seconds)
|
||||||
@@ -93,28 +147,14 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [detectionEngine]);
|
||||||
|
|
||||||
// Initialize ML detection system
|
// Set the detection callback after handleDetection is defined
|
||||||
const {
|
useEffect(() => {
|
||||||
isLoading: isMLLoading,
|
if (setDetectionCallback && handleDetection) {
|
||||||
metrics,
|
setDetectionCallback(handleDetection);
|
||||||
error: mlError,
|
|
||||||
initialize: initializeML,
|
|
||||||
startContinuous,
|
|
||||||
stopContinuous,
|
|
||||||
triggerDetection,
|
|
||||||
updateConfig,
|
|
||||||
config
|
|
||||||
} = useDetection({
|
|
||||||
modelVariant: 'standard', // Start with standard model
|
|
||||||
enableContinuous: true,
|
|
||||||
enableTrigger: true,
|
|
||||||
onDetection: handleDetection,
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('ML Detection Error:', error);
|
|
||||||
}
|
}
|
||||||
});
|
}, [handleDetection, setDetectionCallback]);
|
||||||
|
|
||||||
// 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(() => {
|
||||||
@@ -237,38 +277,55 @@ export default function HomePage() {
|
|||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
if (detectionEnabled && triggerDetection) {
|
if (detectionEnabled && triggerDetection) {
|
||||||
try {
|
try {
|
||||||
console.log('Triggering ML detection...');
|
console.log('🎯 Triggering ML detection...');
|
||||||
const mlResult = await triggerDetection();
|
const mlResult = await triggerDetection();
|
||||||
|
|
||||||
if (mlResult) {
|
if (mlResult) {
|
||||||
// Use the existing detected shoe but with real ML confidence
|
console.log('✅ Shoe detected by ML, calling SKU identification...');
|
||||||
const detected = detectShoe(SHOE_DATABASE);
|
|
||||||
if (detected) {
|
// Call SKU identification with the detected shoe
|
||||||
const updatedHistory = addToHistory(detected);
|
if (videoRef.current && detectionEngine) {
|
||||||
|
try {
|
||||||
|
const sku = await detectionEngine.identifyProductSKU(videoRef.current);
|
||||||
|
console.log('📦 Manual scan SKU result:', sku);
|
||||||
|
setDetectedSKU(sku);
|
||||||
|
|
||||||
|
if (sku) {
|
||||||
|
// Create shoe object with SKU for history
|
||||||
|
const shoeWithSKU: Shoe = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: `Producto ${sku}`,
|
||||||
|
brand: 'Identificado por IA',
|
||||||
|
price: 'Precio por consultar',
|
||||||
|
image: '/placeholder.jpg',
|
||||||
|
confidence: mlResult.confidence,
|
||||||
|
sku: sku,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedHistory = addToHistory(shoeWithSKU);
|
||||||
setHistory(updatedHistory);
|
setHistory(updatedHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPopupOpen(true);
|
||||||
|
} catch (skuError) {
|
||||||
|
console.error('❌ SKU identification failed:', skuError);
|
||||||
|
// Still show popup even if SKU fails
|
||||||
|
setDetectedSKU(null);
|
||||||
setPopupOpen(true);
|
setPopupOpen(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('No shoe detected by ML');
|
console.warn('⚠️ Video or detection engine not available for SKU call');
|
||||||
|
setPopupOpen(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('❌ No shoe detected by ML');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('ML detection failed, using fallback:', error);
|
console.error('❌ ML detection failed:', error);
|
||||||
// Fallback to original random detection
|
|
||||||
const detected = detectShoe(SHOE_DATABASE);
|
|
||||||
if (detected) {
|
|
||||||
const updatedHistory = addToHistory(detected);
|
|
||||||
setHistory(updatedHistory);
|
|
||||||
setPopupOpen(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to original random detection when ML is disabled
|
console.log('⚠️ ML detection is disabled');
|
||||||
const detected = detectShoe(SHOE_DATABASE);
|
|
||||||
if (detected) {
|
|
||||||
const updatedHistory = addToHistory(detected);
|
|
||||||
setHistory(updatedHistory);
|
|
||||||
setPopupOpen(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -790,7 +847,7 @@ export default function HomePage() {
|
|||||||
return (
|
return (
|
||||||
<main className="relative h-screen w-screen bg-black overflow-hidden">
|
<main className="relative h-screen w-screen bg-black overflow-hidden">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} />
|
<ShoeResultsPopup isOpen={isPopupOpen} onOpenChange={setPopupOpen} detectedSKU={detectedSKU} />
|
||||||
<HistorySidebar isOpen={isHistoryOpen} onOpenChange={setHistoryOpen} history={history} onItemClick={handleHistoryItemClick} />
|
<HistorySidebar isOpen={isHistoryOpen} onOpenChange={setHistoryOpen} history={history} onItemClick={handleHistoryItemClick} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ export default function HistorySidebar({ history, isOpen, onOpenChange, onItemCl
|
|||||||
{shoe.name}
|
{shoe.name}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center space-x-2 mt-1">
|
<div className="flex items-center space-x-2 mt-1">
|
||||||
<p className="text-green-400 font-bold text-sm">${shoe.price.toFixed(2)}</p>
|
<p className="text-green-400 font-bold text-sm">${shoe.price}</p>
|
||||||
{shoe.promotions?.find(p => p.originalPrice) && (
|
{shoe.promotions?.find(p => p.originalPrice) && (
|
||||||
<p className="text-gray-500 text-xs line-through">
|
<p className="text-gray-500 text-xs line-through">
|
||||||
${shoe.promotions.find(p => p.originalPrice)?.originalPrice?.toFixed(2)}
|
${shoe.promotions.find(p => p.originalPrice)?.originalPrice}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import { fetchProduct, getProductImages, getProductPricing, getProductVariants,
|
|||||||
interface ShoeResultsPopupProps {
|
interface ShoeResultsPopupProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
detectedSKU?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShoeResultsPopup({ isOpen, onOpenChange }: ShoeResultsPopupProps) {
|
export default function ShoeResultsPopup({ isOpen, onOpenChange, detectedSKU }: ShoeResultsPopupProps) {
|
||||||
const [product, setProduct] = useState<Product | null>(null);
|
const [product, setProduct] = useState<Product | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedVariant, setSelectedVariant] = useState<string>('');
|
const [selectedVariant, setSelectedVariant] = useState<string>('');
|
||||||
@@ -213,16 +214,30 @@ export default function ShoeResultsPopup({ isOpen, onOpenChange }: ShoeResultsPo
|
|||||||
|
|
||||||
{/* Product Name & Price */}
|
{/* Product Name & Price */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{/* SKU Detection Badge */}
|
||||||
|
{detectedSKU && (
|
||||||
|
<div className="bg-green-500/20 border border-green-500/30 rounded-lg p-3 mb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-green-300 font-medium text-sm">✅ Producto Identificado por IA</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Tag size={16} className="text-green-400" />
|
||||||
|
<span className="text-green-100 font-bold">SKU: {detectedSKU}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-white md:text-3xl leading-tight">
|
<h1 className="text-2xl font-bold tracking-tight text-white md:text-3xl leading-tight">
|
||||||
{product.productName}
|
{product.productName}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-baseline gap-4">
|
<div className="flex items-baseline gap-4">
|
||||||
<p className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-400 bg-clip-text text-transparent">
|
<p className="text-3xl font-bold bg-gradient-to-r from-green-400 to-emerald-400 bg-clip-text text-transparent">
|
||||||
${pricing.price.toFixed(2)}
|
${pricing.price}
|
||||||
</p>
|
</p>
|
||||||
{pricing.listPrice > pricing.price && (
|
{pricing.listPrice > pricing.price && (
|
||||||
<p className="text-xl text-gray-500 line-through">
|
<p className="text-xl text-gray-500 line-through">
|
||||||
${pricing.listPrice.toFixed(2)}
|
${pricing.listPrice}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -335,7 +350,7 @@ export default function ShoeResultsPopup({ isOpen, onOpenChange }: ShoeResultsPo
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{variant.price > 0 && (
|
{variant.price > 0 && (
|
||||||
<span className="text-sm text-white/60">
|
<span className="text-sm text-white/60">
|
||||||
${variant.price.toFixed(2)}
|
${variant.price}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { DetectionConfig, DetectionResult, DetectionMetrics, DetectionMode } from './types';
|
import type { DetectionConfig, DetectionResult, DetectionMetrics, DetectionMode } from './types';
|
||||||
import { DetectionWorkerManager } from './detection-worker-manager';
|
import { DetectionWorkerManager } from './detection-worker-manager';
|
||||||
import { detectDeviceCapabilities, getRecommendedConfig } from './device-capabilities';
|
import { detectDeviceCapabilities, getRecommendedConfig } from './device-capabilities';
|
||||||
|
import { skuIdentificationService } from '../sku-identification';
|
||||||
|
|
||||||
// Extend window interface for TensorFlow.js
|
// Extend window interface for TensorFlow.js
|
||||||
declare global {
|
declare global {
|
||||||
@@ -150,6 +151,35 @@ export class DetectionEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify product SKU from detected shoe
|
||||||
|
* @param videoElement - Video element to capture image from
|
||||||
|
* @returns Promise<string | null> - Product SKU if identified successfully
|
||||||
|
*/
|
||||||
|
async identifyProductSKU(videoElement: HTMLVideoElement): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
console.log('🔍 Starting product SKU identification...');
|
||||||
|
|
||||||
|
// Capture high-quality image for SKU identification
|
||||||
|
const imageData = this.captureVideoFrame(videoElement, true);
|
||||||
|
|
||||||
|
// Call SKU identification service
|
||||||
|
const sku = await skuIdentificationService.identifySKU(imageData);
|
||||||
|
|
||||||
|
if (sku) {
|
||||||
|
console.log('✅ Product SKU identified:', sku);
|
||||||
|
} else {
|
||||||
|
console.log('❌ No valid SKU found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return sku;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ SKU identification failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Continuous detection loop
|
* Continuous detection loop
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,14 +19,18 @@ interface UseDetectionReturn {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
initialize: (videoElement: HTMLVideoElement) => Promise<void>;
|
initialize: (videoElement: HTMLVideoElement) => Promise<DetectionEngine>;
|
||||||
startContinuous: () => void;
|
startContinuous: () => void;
|
||||||
stopContinuous: () => void;
|
stopContinuous: () => void;
|
||||||
triggerDetection: () => Promise<DetectionResult | null>;
|
triggerDetection: () => Promise<DetectionResult | null>;
|
||||||
updateConfig: (config: Partial<DetectionConfig>) => Promise<void>;
|
updateConfig: (config: Partial<DetectionConfig>) => Promise<void>;
|
||||||
|
setDetectionCallback: (callback: (detection: DetectionResult | null) => void) => void;
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
config: DetectionConfig | null;
|
config: DetectionConfig | null;
|
||||||
|
|
||||||
|
// Engine reference
|
||||||
|
detectionEngine: DetectionEngine | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +45,9 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
onError
|
onError
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
// Store the callback in a ref so it can be updated
|
||||||
|
const detectionCallbackRef = useRef<((detection: DetectionResult | null) => void) | undefined>(onDetection);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isDetecting, setIsDetecting] = useState(false);
|
const [isDetecting, setIsDetecting] = useState(false);
|
||||||
@@ -55,7 +62,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
const initializationPromiseRef = useRef<Promise<void> | null>(null);
|
const initializationPromiseRef = useRef<Promise<void> | null>(null);
|
||||||
|
|
||||||
// Initialize detection engine
|
// Initialize detection engine
|
||||||
const initialize = useCallback(async (videoElement: HTMLVideoElement): Promise<void> => {
|
const initialize = useCallback(async (videoElement: HTMLVideoElement): Promise<DetectionEngine> => {
|
||||||
console.log('🚀 useDetection.initialize called:', { videoElement: !!videoElement });
|
console.log('🚀 useDetection.initialize called:', { videoElement: !!videoElement });
|
||||||
|
|
||||||
// Prevent multiple initializations
|
// Prevent multiple initializations
|
||||||
@@ -78,7 +85,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
// Set up event listeners
|
// Set up event listeners
|
||||||
engine.onDetection((detection) => {
|
engine.onDetection((detection) => {
|
||||||
setCurrentDetection(detection);
|
setCurrentDetection(detection);
|
||||||
onDetection?.(detection);
|
detectionCallbackRef.current?.(detection);
|
||||||
});
|
});
|
||||||
|
|
||||||
engine.onMetrics((newMetrics) => {
|
engine.onMetrics((newMetrics) => {
|
||||||
@@ -96,6 +103,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
setConfig(initialConfig);
|
setConfig(initialConfig);
|
||||||
|
|
||||||
console.log('Detection hook initialized successfully');
|
console.log('Detection hook initialized successfully');
|
||||||
|
return engine; // Return engine instance
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err instanceof Error ? err : new Error('Unknown initialization error');
|
const error = err instanceof Error ? err : new Error('Unknown initialization error');
|
||||||
@@ -175,7 +183,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
|
|
||||||
// Update current detection state
|
// Update current detection state
|
||||||
setCurrentDetection(detection);
|
setCurrentDetection(detection);
|
||||||
onDetection?.(detection);
|
detectionCallbackRef.current?.(detection);
|
||||||
|
|
||||||
return detection;
|
return detection;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -185,7 +193,7 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
onError?.(error);
|
onError?.(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, [enableTrigger, onDetection, onError]);
|
}, [enableTrigger, onError]);
|
||||||
|
|
||||||
// Update configuration
|
// Update configuration
|
||||||
const updateConfig = useCallback(async (newConfig: Partial<DetectionConfig>): Promise<void> => {
|
const updateConfig = useCallback(async (newConfig: Partial<DetectionConfig>): Promise<void> => {
|
||||||
@@ -233,9 +241,15 @@ export function useDetection(options: UseDetectionOptions = {}): UseDetectionRet
|
|||||||
stopContinuous,
|
stopContinuous,
|
||||||
triggerDetection,
|
triggerDetection,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
|
setDetectionCallback: (callback: (detection: DetectionResult | null) => void) => {
|
||||||
|
detectionCallbackRef.current = callback;
|
||||||
|
},
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
config
|
config,
|
||||||
|
|
||||||
|
// Engine reference
|
||||||
|
detectionEngine: detectionEngineRef.current
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
export type Shoe = {
|
export type Shoe = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
brand?: string;
|
||||||
description: string;
|
price: number | string;
|
||||||
imageUrl: string;
|
description?: string;
|
||||||
colorOptions: { color: string; imageUrl: string }[];
|
image?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
colorOptions?: { color: string; imageUrl: string }[];
|
||||||
|
confidence?: number;
|
||||||
|
sku?: string;
|
||||||
|
timestamp?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SHOE_DATABASE: Shoe[] = [
|
export const SHOE_DATABASE: Shoe[] = [
|
||||||
|
|||||||
224
lib/sku-identification.ts
Normal file
224
lib/sku-identification.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
interface SKUResponse {
|
||||||
|
status: boolean;
|
||||||
|
SKU: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
response: SKUResponse;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for identifying product SKUs from shoe images using external API
|
||||||
|
*/
|
||||||
|
export class SKUIdentificationService {
|
||||||
|
private cache = new Map<string, CacheEntry>();
|
||||||
|
private readonly API_ENDPOINT = 'https://pegasus-working-bison.ngrok-free.app/predictfile';
|
||||||
|
private readonly CACHE_TTL = 60 * 60 * 1000; // 1 hour cache
|
||||||
|
private readonly MAX_CACHE_SIZE = 100; // Prevent memory leaks
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ImageData to File object for form-data upload
|
||||||
|
*/
|
||||||
|
private imageDataToFile(imageData: ImageData, filename: string = 'shoe.jpg'): File {
|
||||||
|
// Create canvas from ImageData
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
canvas.width = imageData.width;
|
||||||
|
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 any; // TypeScript workaround for sync return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a simple hash from image data for caching
|
||||||
|
*/
|
||||||
|
private generateImageHash(imageData: ImageData): string {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean expired cache entries and maintain size limit
|
||||||
|
*/
|
||||||
|
private cleanCache(): void {
|
||||||
|
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
|
||||||
|
if (this.cache.size > this.MAX_CACHE_SIZE) {
|
||||||
|
const sortedEntries = Array.from(this.cache.entries())
|
||||||
|
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||||
|
|
||||||
|
const entriesToRemove = sortedEntries.slice(0, this.cache.size - this.MAX_CACHE_SIZE);
|
||||||
|
for (const [key] of entriesToRemove) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify product SKU from shoe image
|
||||||
|
* @param imageData - Image data captured from video
|
||||||
|
* @returns Promise<string | null> - SKU if found, null otherwise
|
||||||
|
*/
|
||||||
|
async identifySKU(imageData: ImageData): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// Generate hash for caching
|
||||||
|
const imageHash = this.generateImageHash(imageData);
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
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)
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
canvas.width = imageData.width;
|
||||||
|
canvas.height = imageData.height;
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
// Convert canvas to blob and create File
|
||||||
|
const blob = await new Promise<Blob>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.9); // Higher quality
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = new File([blob], 'shoe.jpg', {
|
||||||
|
type: 'image/jpeg',
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📸 Created file:', {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
lastModified: file.lastModified
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare form data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
console.log('📤 FormData prepared with file key');
|
||||||
|
|
||||||
|
// Debug FormData contents
|
||||||
|
for (let [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
|
||||||
|
const response = await fetch(this.API_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
mode: 'cors', // Need CORS headers from server
|
||||||
|
headers: {
|
||||||
|
// Don't set Content-Type, let browser set it with boundary for multipart/form-data
|
||||||
|
'ngrok-skip-browser-warning': 'true', // Skip ngrok browser warning
|
||||||
|
'Accept': 'application/json', // Tell server we expect JSON
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📡 Response status:', response.status, response.statusText);
|
||||||
|
console.log('📡 Response headers:', Object.fromEntries(response.headers.entries()));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API call failed: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get response as text first to debug
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log('📡 Raw response text:', responseText);
|
||||||
|
|
||||||
|
// Try to parse as JSON
|
||||||
|
let data: SKUResponse;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(responseText);
|
||||||
|
console.log('📦 SKU API response parsed:', data);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('❌ Failed to parse JSON response:', parseError);
|
||||||
|
console.error('❌ Response was:', responseText);
|
||||||
|
throw new Error(`Invalid JSON response: ${parseError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the response (both success and failure)
|
||||||
|
this.cleanCache();
|
||||||
|
this.cache.set(imageHash, {
|
||||||
|
response: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return SKU only if status is true and SKU is not null
|
||||||
|
return data.status && data.SKU ? data.SKU : null;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ SKU identification failed:', error);
|
||||||
|
// Return null on any error (network, parsing, 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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const skuIdentificationService = new SKUIdentificationService();
|
||||||
40
lib/sku-prediction-api.ts
Normal file
40
lib/sku-prediction-api.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Communicates with the SKU prediction API to get a product SKU from an image.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an image file to the prediction endpoint and returns a product SKU.
|
||||||
|
*
|
||||||
|
* @param imageFile The image file to be sent for prediction.
|
||||||
|
* @returns A promise that resolves to the product SKU string if the prediction is successful, otherwise null.
|
||||||
|
*/
|
||||||
|
export async function getSkuFromImage(imageFile: File): Promise<string | null> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", imageFile);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://pegasus-working-bison.ngrok-free.app/predictfile",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("SKU prediction API request failed:", response.status, response.statusText);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.status === true && typeof result.SKU === "string" && result.SKU) {
|
||||||
|
return result.SKU;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error calling SKU prediction API:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack --port=3011",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
|
|||||||
Reference in New Issue
Block a user