"""Prepare the listing text for the text-only extractor: concatenate description, features and broker details, and MASK every price, tax and municipal-assessment amount so that the text model cannot read value information (the WP14 leakage lesson: DuProprio descriptions embed the asking price). """ from __future__ import annotations import json import re MASKED_KEYS = re.compile(r"(évaluation municipale|evaluation municipale|municipal assessment|taxes?|prix|price|revenus|" r"asking|hypoth|mortgage|frais de condo|condo fees|img_audited|transaction)", re.I) MONEY = re.compile(r"(\$\s?\d[\d\s.,]*\d|\d[\d\s]{2,}(?:[.,]\d+)?\s?(?:\$|dollars|k\$|CAD))", re.I) ASSESS_SENT = re.compile(r"(municipal assessment|évaluation municipale|evaluation municipale|asking price|prix demandé|" r"taxes municipales|taxes scolaires|school taxes|municipal taxes)[^.\n]{0,80}", re.I) def mask_money(s: str) -> str: s = ASSESS_SENT.sub("[masked]", s) return MONEY.sub("[amount]", s) def build_text(row) -> tuple[str, dict]: """Return (masked text, stats). `row` must have description, features (JSON list), details (JSON dict).""" parts = [] desc = row.get("description") or "" parts.append("DESCRIPTION:\n" + mask_money(desc)) try: feats = json.loads(row.get("features") or "[]") except Exception: feats = [] if isinstance(feats, list) and feats: feats = [mask_money(str(f)) for f in feats[:80] if not MASKED_KEYS.search(str(f).split(":")[0])] parts.append("FEATURES: " + "; ".join(feats)) try: det = json.loads(row.get("details") or "{}") except Exception: det = {} kept, dropped = [], 0 if isinstance(det, dict): for k, v in det.items(): if MASKED_KEYS.search(str(k)): dropped += 1 continue if k == "pieces" and isinstance(v, list): rooms = "; ".join(f"{p.get('nom','')} ({p.get('niveau','')}, {p.get('dimensions','')}, {p.get('revetement','')})" for p in v[:25] if isinstance(p, dict)) kept.append(f"ROOMS: {rooms}") elif isinstance(v, (str, int, float)): if k == "remarques_proprio" and str(v).strip()[:200] == desc.strip()[:200]: continue # duplicate of the description kept.append(f"{k}: {mask_money(str(v))}") if kept: parts.append("BROKER FIELDS:\n" + "\n".join(kept)) text = "\n\n".join(parts) stats = {"desc_chars": len(desc), "n_features": len(feats) if isinstance(feats, list) else 0, "n_detail_kept": len(kept), "n_detail_masked_keys": dropped, "n_amounts_masked": len(MONEY.findall(desc)) + len(ASSESS_SENT.findall(desc))} return text[:12000], stats