mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-20 05:38:12 +02:00
feat: Introduce AI extraction schema with validation, prompt generation, and a verification script to improve price extraction reliability.
This commit is contained in:
1 parent
ef7cd7c1cf
commit
384baa3575
4 files changed
+230
-5
No files matched your search
+4
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in new issue
Block a user