- Add support for PNG, JPEG, and WebP image formats in flyer import - Replace external importer service with internal AI-based parsing pipeline - Add new services: TextExtractorService, AiFlyerParserService, FlyerNormalizerService - Integrate Mistral AI, pdf-parse, and tesseract.js dependencies - Add quality confidence indicators and warning panels in Flutter UI - Update package.json with new dependencies and transform ignore patterns - Add documentation for flyer importer system - Add Kilo AI planning file for Happy Island project BREAKING CHANGE: Flyer import now uses internal AI parsing instead of external importer service
9.9 KiB
flyerimporter.md 📌 Steg 1: Skapa en funktion för att extrahera text från PDF:en Använd pdf-parse som primär metod och Tesseract.js som fallback för OCR. Kod: extractFlyerText.ts typescript Copy
import * as fs from 'fs'; import * as pdf from 'pdf-parse'; import Tesseract from 'tesseract.js';
/**
- Extraherar text från en PDF-fil (flyer), med fallback till OCR.
- @param pdfPath Sökväg till PDF-filen.
- @returns Extraherad text. */ export async function extractFlyerText(pdfPath: string): Promise { try { // Försök med pdf-parse först const dataBuffer = fs.readFileSync(pdfPath); const data = await pdf(dataBuffer); if (data.text.trim()) { return data.text; } } catch (error) { console.warn('pdf-parse misslyckades, försöker med OCR...'); }
// Fallback till Tesseract.js för OCR try { const { data: { text } } = await Tesseract.recognize(pdfPath, 'swe', { logger: (m) => console.log(m), }); return text; } catch (error) { console.error('OCR misslyckades:', error); throw new Error('Kunde inte extrahera text från PDF:en.'); } }
📌 Steg 2: Skapa en funktion för att skicka texten till Mistral Tiny Använd Mistral Tiny för att extrahera och strukturera all produktinformation från flyern. Kod: importFlyerWithAI.ts typescript Copy
import { MistralClient } from '@mistralai/mistralai';
const mistral = new MistralClient({ apiKey: process.env.MISTRAL_API_KEY, });
/**
-
Skickar flyer-texten till Mistral Tiny för att extrahera strukturerad data.
-
@param text Texten från flyern.
-
@returns Strukturerad data (JSON-array). */ export async function importFlyerWithAI(text: string): Promise<any[]> { const prompt = ` Du är en expert på att tolka svenska matvaruflyers (t.ex. från Willys). Extrahera ALL produktinformation från följande text och returnera den som en JSON-array.
För varje produkt, inkludera:
- name: Produktnamn (fullständigt namn)
- weight: Vikt (om tillgänglig, t.ex. "150g", "Ca 1kg")
- origin: Ursprung/land/märke (om tillgänglig, t.ex. "FALKENBERG", "NYBERGS DELI • Sverige")
- price: Pris (som ett nummer, t.ex. 39.90)
- comparisonPrice: Jämförpris (som ett nummer, t.ex. 266.00)
- unit: Enhet (kg, st, förp, l, etc.)
- offer: Erbjudande (t.ex. ["Max 3 köp/hushåll", "Lägsta 30-dgrspris 125:00 kr"])
- category: Kategori (t.ex. "Fisk", "Kött", "Mejeri", "Grönsaker", "Frukt", "Dryck")
- validFrom: Giltig från (datum i formatet YYYY-MM-DD, om tillgängligt)
- validTo: Giltig till (datum i formatet YYYY-MM-DD, om tillgängligt)
Texten att tolka: ${text}
Returnera ENDAST en JSON-array. Inga andra kommentarer. Exempel på utdata: [ { "name": "KALLRÖKT LAX, GRAVAD LAX", "weight": "150g", "origin": "FALKENBERG", "price": 39.90, "comparisonPrice": 266.00, "unit": "kg", "offer": ["Max 3 köp/hushåll"], "category": "Fisk", "validFrom": "2026-05-18", "validTo": "2026-05-24" } ] `;
try { const response = await mistral.chat({ model: 'mistral-tiny', // Använder den enklaste modellen messages: [{ role: 'user', content: prompt }], temperature: 0.1, // Låg temperatur för mer deterministiska svar });
// Rensa upp JSON-strängen
const jsonString = response.choices[0].message.content
.replace(/```json|```/g, '')
.trim();
// Parsa JSON:en
return JSON.parse(jsonString);
} catch (error) { console.error('Fel vid AI-import:', error); throw new Error('Kunde inte importera flyern med AI.'); } }
📌 Steg 3: Fullständigt importflöde Kombinera text-extrahering och AI-import i ett fullständigt flöde. Kod: flyerImportService.ts typescript Copy
import { extractFlyerText } from './extractFlyerText'; import { importFlyerWithAI } from './importFlyerWithAI';
/**
-
Importerar en flyer (PDF) och returnerar strukturerad data.
-
@param pdfPath Sökväg till PDF-filen.
-
@returns Strukturerad data från flyern. */ export async function importFlyer(pdfPath: string) { try { // 1. Extrahera text från PDF:en console.log('Extraherar text från flyern...'); const text = await extractFlyerText(pdfPath);
// 2. Skicka texten till Mistral Tiny för att extrahera data console.log('Skickar text till Mistral Tiny för extrahering...'); const products = await importFlyerWithAI(text);
return { success: true, products, text, }; } catch (error) { console.error('Fel vid import:', error); return { success: false, error: error instanceof Error ? error.message : 'Okänt fel', }; } }
📌 Steg 4: API-Endpoint för flyer-import Skapa en Express-endpoint för att hantera uppladdning och import av flyers. Kod: flyerImportRouter.ts typescript Copy
import express from 'express'; import multer from 'multer'; import { importFlyer } from '../services/flyerImportService'; import * as fs from 'fs';
const router = express.Router(); const upload = multer({ dest: 'uploads/' });
// Endpoint för att ladda upp och importera en flyer router.post('/import/flyer', upload.single('flyer'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ error: 'Ingen flyer uppladdad.' }); }
const result = await importFlyer(req.file.path);
// Rensa upp uppladdad fil
fs.unlinkSync(req.file.path);
if (!result.success) {
return res.status(500).json({ error: result.error });
}
res.json(result);
} catch (error) { console.error('Fel vid flyer-import:', error); res.status(500).json({ error: 'Kunde inte importera flyern.' }); } });
export default router;
📌 Steg 5: Exempel på utdata När du skickar texten från Willys flyer till Mistral Tiny, kommer du att få tillbaka en JSON-array med alla produkter, t.ex.: json Copy
[ { "name": "KALLRÖKT LAX, GRAVAD LAX", "weight": "150g", "origin": "FALKENBERG", "price": 39.9, "comparisonPrice": 266.0, "unit": "kg", "offer": ["Max 3 köp/hushåll"], "category": "Fisk", "validFrom": "2026-05-18", "validTo": "2026-05-24" }, { "name": "FLÄSKYTTERFILÉ", "weight": "Ca 1kg", "origin": "NYBERGS DELI • Sverige", "price": 64.9, "comparisonPrice": 64.9, "unit": "kg", "offer": ["Max 3 förp/hushåll"], "category": "Kött", "validFrom": "2026-05-18", "validTo": "2026-05-24" } ]
📌 Steg 6: Använda den extraherade datan När du har den strukturerade datan, kan du:
Lagra den i din databas (t.ex. för att jämföra med inventory). Visa den för användaren (t.ex. i en tabell). Använda den för att generera recept (med eller utan AI). Exempel: Lagra i databasen typescript Copy
// Antas att du har en Prisma-modell för flyer-produkter await prisma.flyerProduct.createMany({ data: products.map((product) => ({ name: product.name, weight: product.weight, origin: product.origin, price: product.price, comparisonPrice: product.comparisonPrice, unit: product.unit, offer: JSON.stringify(product.offer), category: product.category, validFrom: product.validFrom ? new Date(product.validFrom) : null, validTo: product.validTo ? new Date(product.validTo) : null, })), });
📌 Steg 7: Frontend-Integrering (Exempel: React) Här är hur du kan integrera flyer-importen i din frontend: Kod: FlyerImportForm.tsx tsx Copy
import { useState } from 'react'; import axios from 'axios';
function FlyerImportForm() { const [file, setFile] = useState<File | null>(null); const [isLoading, setIsLoading] = useState(false); const [result, setResult] = useState(null);
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) return;
setIsLoading(true);
const formData = new FormData();
formData.append('flyer', file);
try {
const response = await axios.post('/api/import/flyer', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
setResult(response.data);
} catch (error) {
console.error('Fel vid uppladdning:', error);
} finally {
setIsLoading(false);
}
};
return (
Importera flyer
<input type="file" accept=".pdf" onChange={(e) => setFile(e.target.files?.[0] || null)} required /> {isLoading ? 'Importerar...' : 'Importera flyer'} {result?.success && (
<div>
<h3>Importerade produkter ({result.products.length})</h3>
<table>
<thead>
<tr>
<th>Namn</th>
<th>Pris</th>
<th>Jämförpris</th>
<th>Kategori</th>
<th>Erbjudande</th>
</tr>
</thead>
<tbody>
{result.products.map((product: any, index: number) => (
<tr key={index}>
<td>{product.name}</td>
<td>{product.price} {product.unit}</td>
<td>{product.comparisonPrice} {product.unit}</td>
<td>{product.category}</td>
<td>{product.offer.join(', ')}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
); }
export default FlyerImportForm;
📌 Miljövariabler (.env) env Copy
Mistral API-nyckel
MISTRAL_API_KEY=din_api_nyckel_här