feat: Introduce AI extraction schema with validation, prompt generation, and a verification script to improve price extraction reliability.

This commit is contained in:
Michael committed 2025-12-18 16:32:32 +01:00
1 parent ef7cd7c1cf
commit 384baa3575
4 files changed
+230 -5

No files matched your search

+4 -2
View File
@@ -141,7 +141,9 @@ EXTRACTION_PROMPT_TEMPLATE = """Extract product price and stock status from this
- Look for: "PRIX DÉTECTÉ:", price tags, "Prix:", "€", numbers near "Ajouter au panier"
- Extract as DECIMAL NUMBER: If you see "3.99", return 3.99
- Ignore crossed-out/barré prices (old prices)
- If multiple prices, take the current/main price (not the original)
- **CRITICAL:** Ignore "Prix au litre", "Prix au kg", "P.U.", or unit prices usually shown in smaller text/parentheses (e.g., "4.60 € / L").
- If multiple prices, take the current/main price (not the original, not the unit price)
- **B&M STORES Specific:** The main price is often large and bold (e.g. "1.15€"), while unit price is small (e.g. "4.60 €/L"). ALWAYS take the main price.
**CRITICAL - Common mistakes to avoid:**
- "3.99 €" means 3.99 (NOT 399.00, NOT 3990.00)
@@ -161,7 +163,7 @@ EXTRACTION_PROMPT_TEMPLATE = """Extract product price and stock status from this
- If unclear: set null and confidence < 0.5
**STOCK:**
- TRUE if: "Ajouter au panier", "Acheter", "En stock", "Disponible", "Add to Cart"
- TRUE if: "Ajouter au panier", "Acheter", "En stock", "Disponible", "Add to Cart", "Retrait 2h", "Click & Collect"
- FALSE if: "Rupture", "Indisponible", "Épuisé", "Out of Stock", "Notify Me"
- NULL if unclear or not shown
+34 -3
View File
@@ -416,9 +416,40 @@ class ScraperService:
try:
logger.info(f"Extracting text (limit: {text_length} chars)...")
raw_text = await page.inner_text("body")
page_text = raw_text[:text_length]
logger.info(f"Extracted {len(page_text)} characters")
# Smart extraction: remove noise (nav, footer, scripts) before getting text
clean_text = await page.evaluate("""
() => {
// Clone body to not affect the visual page
const clone = document.body.cloneNode(true);
// Remove noise selectors
const noiseSelectors = [
'nav', 'header', 'footer', 'script', 'style', 'noscript', 'iframe',
'.cookie-banner', '.popup', '#menu', '.menu', '.sidebar',
'[role="navigation"]', '[role="banner"]', '[role="contentinfo"]'
];
noiseSelectors.forEach(selector => {
const elements = clone.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
return clone.innerText;
}
""")
# Fallback if cleaning removed everything (unlikely but safe)
if not clean_text or len(clean_text) < 100:
logger.warning("Cleaned text too short, falling back to full body text")
clean_text = await page.inner_text("body")
# Collapse whitespace
import re
clean_text = re.sub(r'\s+', ' ', clean_text).strip()
page_text = clean_text[:text_length]
logger.info(f"Extracted {len(page_text)} chars")
return page_text
except Exception as e:
logger.error(f"Text extraction failed: {e}")