feat: Add Spotify-style backdrop, Cascade OCR, Smart Scan Flow & OCR Dashboard

- BottleGrid: Implement blurred backdrop effect for bottle cards
- Cascade OCR: TextDetector → RegEx → Fuzzy Match → window.ai pipeline
- Smart Scan: Native OCR for Android, Live Text fallback for iOS
- OCR Dashboard: Admin page at /admin/ocr-logs with stats and scan history
- Features: Add feature flags in src/config/features.ts
- SQL: Add ocr_logs table migration
- Services: Update analyze-bottle to use OpenRouter, add save-ocr-log
This commit is contained in:
2026-01-18 20:38:48 +01:00
parent 83e852e5fb
commit 9ba0825bcd
46 changed files with 3874 additions and 741 deletions

View File

@@ -0,0 +1,123 @@
// Background Removal Worker using briaai/RMBG-1.4
// Using @huggingface/transformers v3
import { AutoModel, AutoProcessor, RawImage, env } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.5.1';
console.log('[BG-Processor Worker] Script loaded from /public');
// Configuration
env.allowLocalModels = false;
env.useBrowserCache = true;
// Force WASM backend (more compatible)
env.backends.onnx.wasm.proxy = false;
let model = null;
let processor = null;
/**
* Load the RMBG-1.4 model (WASM only for compatibility)
*/
const loadModel = async () => {
if (!model) {
console.log('[BG-Processor Worker] Loading briaai/RMBG-1.4 model (WASM)...');
model = await AutoModel.from_pretrained('briaai/RMBG-1.4', {
device: 'wasm',
dtype: 'fp32',
});
processor = await AutoProcessor.from_pretrained('briaai/RMBG-1.4');
console.log('[BG-Processor Worker] Model loaded successfully.');
}
return { model, processor };
};
/**
* Apply the alpha mask to the original image
*/
const applyMask = async (originalBlob, maskData, width, height) => {
const bitmap = await createImageBitmap(originalBlob);
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error("No Canvas context");
// Draw original image
ctx.drawImage(bitmap, 0, 0);
// Get image data
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
const data = imageData.data;
// Create mask canvas at model output size
const maskCanvas = new OffscreenCanvas(width, height);
const maskCtx = maskCanvas.getContext('2d');
const maskImageData = maskCtx.createImageData(width, height);
// Convert model output to grayscale image
for (let i = 0; i < maskData.length; i++) {
const val = Math.round(Math.max(0, Math.min(1, maskData[i])) * 255);
maskImageData.data[i * 4] = val;
maskImageData.data[i * 4 + 1] = val;
maskImageData.data[i * 4 + 2] = val;
maskImageData.data[i * 4 + 3] = 255;
}
maskCtx.putImageData(maskImageData, 0, 0);
// Scale mask to original size
const scaledMaskCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
const scaledMaskCtx = scaledMaskCanvas.getContext('2d');
scaledMaskCtx.drawImage(maskCanvas, 0, 0, bitmap.width, bitmap.height);
const scaledMaskData = scaledMaskCtx.getImageData(0, 0, bitmap.width, bitmap.height);
// Apply mask as alpha
for (let i = 0; i < data.length; i += 4) {
data[i + 3] = scaledMaskData.data[i]; // Use R channel as alpha
}
ctx.putImageData(imageData, 0, 0);
return await canvas.convertToBlob({ type: 'image/png' });
};
self.onmessage = async (e) => {
const { type, id, imageBlob } = e.data;
if (type === 'ping') {
self.postMessage({ type: 'pong' });
return;
}
if (!imageBlob) return;
console.log(`[BG-Processor Worker] Received request for ${id}`);
try {
const { model, processor } = await loadModel();
// Convert blob to RawImage
const url = URL.createObjectURL(imageBlob);
const image = await RawImage.fromURL(url);
URL.revokeObjectURL(url);
console.log('[BG-Processor Worker] Running inference...');
// Process image
const { pixel_values } = await processor(image);
// Run model
const { output } = await model({ input: pixel_values });
// Get mask data - output is a Tensor
const maskData = output.data;
const [batch, channels, height, width] = output.dims;
console.log(`[BG-Processor Worker] Mask dims: ${width}x${height}`);
console.log('[BG-Processor Worker] Applying mask...');
const processedBlob = await applyMask(imageBlob, maskData, width, height);
self.postMessage({ id, status: 'success', blob: processedBlob });
console.log(`[BG-Processor Worker] Successfully processed ${id}`);
} catch (err) {
console.error(`[BG-Processor Worker] Processing Error (${id}):`, err);
self.postMessage({ id, status: 'error', error: err.message });
}
};