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:
123
public/bg-processor.worker.js
Normal file
123
public/bg-processor.worker.js
Normal 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 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user