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:
2025-12-25 23:39:08 +01:00
parent 462d27ea7b
commit f0f36e9c03
17 changed files with 55 additions and 2190 deletions

View File

@@ -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>