perf: Remove Tesseract OCR - saves ~45MB on mobile
- Removed Tesseract.js files from precache (~45MB) - Scanner now uses only Gemini AI (more accurate, less data) - Offline scans queued for later processing when online - App download from ~50MB to ~5MB BREAKING: Local offline OCR no longer available Use Gemini AI instead (requires network for scanning)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, Loader2, Sparkles, AlertCircle, Clock, Eye, Cloud, Cpu } from 'lucide-react';
|
||||
import { X, Loader2, Sparkles, AlertCircle, Clock, Cloud } from 'lucide-react';
|
||||
import TastingEditor from './TastingEditor';
|
||||
import SessionBottomSheet from './SessionBottomSheet';
|
||||
import ResultCard from './ResultCard';
|
||||
@@ -40,26 +40,11 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
const [isEnriching, setIsEnriching] = useState(false);
|
||||
const [aiFallbackActive, setAiFallbackActive] = useState(false);
|
||||
|
||||
// Use the new hybrid scanner hook
|
||||
// Use the Gemini-only scanner hook
|
||||
const scanner = useScanner({
|
||||
locale,
|
||||
onLocalComplete: (localResult) => {
|
||||
console.log('[ScanFlow] Local OCR complete, updating preview:', localResult);
|
||||
// Immediately update bottleMetadata with local results for optimistic UI
|
||||
setBottleMetadata(prev => ({
|
||||
...prev,
|
||||
name: localResult.name || prev?.name || null,
|
||||
distillery: localResult.distillery || prev?.distillery || null,
|
||||
abv: localResult.abv ?? prev?.abv ?? null,
|
||||
age: localResult.age ?? prev?.age ?? null,
|
||||
vintage: localResult.vintage || prev?.vintage || null,
|
||||
is_whisky: true,
|
||||
confidence: 50,
|
||||
} as BottleMetadata));
|
||||
},
|
||||
onCloudComplete: (cloudResult) => {
|
||||
console.log('[ScanFlow] Cloud vision complete:', cloudResult);
|
||||
// Update with cloud results (this is the "truth")
|
||||
onComplete: (cloudResult) => {
|
||||
console.log('[ScanFlow] Gemini complete:', cloudResult);
|
||||
setBottleMetadata(cloudResult);
|
||||
|
||||
// Trigger background enrichment if we have name and distillery
|
||||
@@ -153,30 +138,20 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
|
||||
if (scannerStatus === 'idle') {
|
||||
// Don't change state on idle
|
||||
} else if (scannerStatus === 'compressing' || scannerStatus === 'analyzing_local') {
|
||||
} else if (scannerStatus === 'compressing' || scannerStatus === 'analyzing') {
|
||||
setState('SCANNING');
|
||||
} else if (scannerStatus === 'analyzing_cloud') {
|
||||
// Show EDITOR immediately when we have local results - don't wait for cloud
|
||||
if (scanner.localResult || scanner.mergedResult) {
|
||||
setState('EDITOR');
|
||||
} else {
|
||||
setState('SCANNING');
|
||||
}
|
||||
} else if (scannerStatus === 'complete' || scannerStatus === 'queued') {
|
||||
// Use merged result from scanner
|
||||
if (scanner.mergedResult) {
|
||||
setBottleMetadata(scanner.mergedResult);
|
||||
}
|
||||
setState('EDITOR');
|
||||
|
||||
// If this was a queued offline scan, mark as fallback
|
||||
if (scannerStatus === 'queued') {
|
||||
setAiFallbackActive(true);
|
||||
setIsOffline(true);
|
||||
}
|
||||
} else if (scannerStatus === 'error') {
|
||||
if (scanner.mergedResult) {
|
||||
// We have partial results, show editor anyway
|
||||
setBottleMetadata(scanner.mergedResult);
|
||||
setState('EDITOR');
|
||||
setAiFallbackActive(true);
|
||||
@@ -185,7 +160,7 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
setState('ERROR');
|
||||
}
|
||||
}
|
||||
}, [scanner.status, scanner.mergedResult, scanner.localResult, scanner.error]);
|
||||
}, [scanner.status, scanner.mergedResult, scanner.error]);
|
||||
|
||||
const handleScan = async (file: File) => {
|
||||
setState('SCANNING');
|
||||
@@ -206,7 +181,6 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
const tempId = `temp_${Date.now()}`;
|
||||
const bottleDataToSave = formData.bottleMetadata || bottleMetadata;
|
||||
|
||||
// Check for existing pending scan with same image
|
||||
const existingScan = await db.pending_scans
|
||||
.filter(s => s.imageBase64 === scanner.processedImage!.base64)
|
||||
.first();
|
||||
@@ -226,7 +200,6 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
});
|
||||
}
|
||||
|
||||
// Save pending tasting linked to temp bottle
|
||||
await db.pending_tastings.add({
|
||||
pending_bottle_id: currentTempId,
|
||||
data: {
|
||||
@@ -258,13 +231,11 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
if (authError.message?.includes('Failed to fetch') || authError.message?.includes('NetworkError')) {
|
||||
console.log('[ScanFlow] Auth failed due to network - switching to offline mode');
|
||||
setIsOffline(true);
|
||||
// Retry save in offline mode
|
||||
return handleSaveTasting(formData);
|
||||
}
|
||||
throw authError;
|
||||
}
|
||||
|
||||
// Save Bottle
|
||||
const bottleDataToSave = formData.bottleMetadata || bottleMetadata;
|
||||
const bottleResult = await saveBottle(bottleDataToSave, scanner.processedImage.base64, user.id);
|
||||
if (!bottleResult.success || !bottleResult.data) {
|
||||
@@ -273,7 +244,6 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
|
||||
const bottleId = bottleResult.data.id;
|
||||
|
||||
// Save Tasting
|
||||
const tastingNote = {
|
||||
...formData,
|
||||
bottle_id: bottleId,
|
||||
@@ -314,14 +284,11 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
}
|
||||
};
|
||||
|
||||
// Map scanner status to display text
|
||||
const getScanStatusDisplay = (status: ScanStatus): { text: string; icon: React.ReactNode } => {
|
||||
switch (status) {
|
||||
case 'compressing':
|
||||
return { text: 'Bild optimieren...', icon: <Loader2 size={12} className="animate-spin" /> };
|
||||
case 'analyzing_local':
|
||||
return { text: 'Lokale OCR-Analyse...', icon: <Cpu size={12} /> };
|
||||
case 'analyzing_cloud':
|
||||
case 'analyzing':
|
||||
return { text: 'KI-Vision-Analyse...', icon: <Cloud size={12} /> };
|
||||
default:
|
||||
return { text: 'Analysiere Etikett...', icon: <Loader2 size={12} className="animate-spin" /> };
|
||||
@@ -373,13 +340,10 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
</p>
|
||||
{/* Show scan stage indicators */}
|
||||
<div className="flex items-center justify-center gap-2 mt-4">
|
||||
<div className={`w-2 h-2 rounded-full transition-colors ${['compressing', 'analyzing_local', 'analyzing_cloud', 'complete'].includes(scanner.status)
|
||||
<div className={`w-2 h-2 rounded-full transition-colors ${['compressing', 'analyzing', 'complete'].includes(scanner.status)
|
||||
? 'bg-orange-500' : 'bg-zinc-700'
|
||||
}`} />
|
||||
<div className={`w-2 h-2 rounded-full transition-colors ${['analyzing_local', 'analyzing_cloud', 'complete'].includes(scanner.status)
|
||||
? 'bg-orange-500' : 'bg-zinc-700'
|
||||
}`} />
|
||||
<div className={`w-2 h-2 rounded-full transition-colors ${['analyzing_cloud', 'complete'].includes(scanner.status)
|
||||
<div className={`w-2 h-2 rounded-full transition-colors ${['analyzing', 'complete'].includes(scanner.status)
|
||||
? 'bg-orange-500' : 'bg-zinc-700'
|
||||
}`} />
|
||||
</div>
|
||||
@@ -389,15 +353,11 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
{/* Admin perf metrics */}
|
||||
{isAdmin && scanner.perf && (
|
||||
<div className="mt-8 p-6 bg-zinc-950/80 backdrop-blur-xl rounded-3xl border border-orange-500/20 text-[10px] font-mono text-zinc-400 animate-in fade-in slide-in-from-bottom-4 shadow-2xl">
|
||||
<div className="grid grid-cols-3 gap-6 text-center">
|
||||
<div className="grid grid-cols-2 gap-6 text-center">
|
||||
<div>
|
||||
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">Compress</p>
|
||||
<p className="text-orange-500 font-bold">{scanner.perf.compression.toFixed(0)}ms</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">Local OCR</p>
|
||||
<p className="text-orange-500 font-bold">{scanner.perf.localOcr.toFixed(0)}ms</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-500 mb-2 uppercase tracking-widest text-[8px]">Cloud Vision</p>
|
||||
<p className="text-orange-500 font-bold">{scanner.perf.cloudVision.toFixed(0)}ms</p>
|
||||
@@ -454,22 +414,6 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Local preview indicator */}
|
||||
{scanner.status === 'analyzing_cloud' && scanner.localResult && (
|
||||
<div className="bg-blue-500/10 border-b border-blue-500/20 p-3">
|
||||
<div className="max-w-2xl mx-auto flex items-center gap-3">
|
||||
<Eye size={14} className="text-blue-500" />
|
||||
<p className="text-xs font-bold text-blue-500 uppercase tracking-wider flex items-center gap-2">
|
||||
Lokale Vorschau
|
||||
<span className="flex items-center gap-1 text-zinc-400 font-normal normal-case">
|
||||
<Loader2 size={10} className="animate-spin" />
|
||||
KI-Vision verfeinert Ergebnisse...
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TastingEditor
|
||||
bottleMetadata={bottleMetadata}
|
||||
image={scanner.processedImage?.base64 || null}
|
||||
@@ -481,7 +425,7 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
defaultExpanded={true}
|
||||
/>
|
||||
|
||||
{/* Admin perf overlay - positioned at bottom */}
|
||||
{/* Admin perf overlay */}
|
||||
{isAdmin && scanner.perf && (
|
||||
<div className="fixed bottom-24 left-6 right-6 z-50 p-3 bg-zinc-950/80 backdrop-blur-md rounded-2xl border border-orange-500/20 text-[9px] font-mono text-white/90 shadow-xl overflow-x-auto">
|
||||
<div className="flex items-center justify-between gap-4 whitespace-nowrap">
|
||||
@@ -490,10 +434,6 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS
|
||||
<span className="text-zinc-500">COMPRESS:</span>
|
||||
<span className="text-orange-500 font-bold">{scanner.perf.compression.toFixed(0)}ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-zinc-500">LOCAL:</span>
|
||||
<span className="text-blue-500 font-bold">{scanner.perf.localOcr.toFixed(0)}ms</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-zinc-500">CLOUD:</span>
|
||||
<span className="text-green-500 font-bold">{scanner.perf.cloudVision.toFixed(0)}ms</span>
|
||||
|
||||
Reference in New Issue
Block a user