- 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
124 lines
4.1 KiB
JavaScript
124 lines
4.1 KiB
JavaScript
// 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 });
|
|
}
|
|
};
|