2026-07-25 19:43:03 -04:00
|
|
|
"""Détection de la pertinence guadeloupéenne et ultramarine d'un texte.
|
|
|
|
|
|
|
|
|
|
Le tableau A de l'annexe cote lui-même les 27 lois promulguées ; le tableau B
|
|
|
|
|
ne cote pas les textes en cours. Pour ces derniers, la cotation est déduite du
|
|
|
|
|
vocabulaire employé par le rapport, avec des poids déclarés dans
|
|
|
|
|
`pipeline/config.yaml`.
|
|
|
|
|
|
|
|
|
|
Toute cotation obtenue ainsi est signalée : le texte porte `a_verifier = 1` et
|
|
|
|
|
un motif explicite. L'interface l'affiche comme telle. Une cotation déduite
|
|
|
|
|
n'est pas une cotation établie.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import functools
|
|
|
|
|
import re
|
2026-07-25 22:34:22 -04:00
|
|
|
from dataclasses import dataclass, field
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from pipeline import chemins
|
|
|
|
|
from pipeline.modeles import Pertinence
|
|
|
|
|
from pipeline.parseurs.dates_fr import sans_accents
|
|
|
|
|
|
2026-07-25 22:34:22 -04:00
|
|
|
# Tournures par lesquelles le corpus **écarte** un territoire qu'il vient de
|
|
|
|
|
# nommer : « PPL montagne — sans portée pour la Guadeloupe », « PJL logement —
|
|
|
|
|
# pas de volet outre-mer à ce stade ». Compter la mention sans lire la négation
|
|
|
|
|
# donnerait à ces textes la cotation la plus forte, à l'exact inverse de ce que
|
|
|
|
|
# la source affirme.
|
|
|
|
|
NEGATIONS = (
|
|
|
|
|
"sans portee",
|
|
|
|
|
"sans objet",
|
|
|
|
|
"sans incidence",
|
|
|
|
|
"sans specificite",
|
|
|
|
|
"sans volet",
|
|
|
|
|
"sans consequence",
|
|
|
|
|
"ne concerne pas",
|
|
|
|
|
"n'est pas concerne",
|
|
|
|
|
"pas concernee",
|
|
|
|
|
"pas de volet",
|
|
|
|
|
"pas d'incidence",
|
|
|
|
|
"non concerne",
|
|
|
|
|
"hors champ",
|
|
|
|
|
"exclue",
|
|
|
|
|
"exclus",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Fenêtre de voisinage, en caractères, autour d'une mention de territoire.
|
|
|
|
|
_AVANT = 90
|
|
|
|
|
_APRES = 60
|
|
|
|
|
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class Cotation:
|
|
|
|
|
"""Résultat d'une détection, avec ce qui l'a motivée."""
|
|
|
|
|
|
|
|
|
|
pertinence: Pertinence | None
|
|
|
|
|
score: int
|
|
|
|
|
expressions: list[str]
|
2026-07-25 22:34:22 -04:00
|
|
|
exclusions: list[str] = field(default_factory=list)
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def note(self) -> str | None:
|
2026-07-25 22:34:22 -04:00
|
|
|
if self.exclusions and not self.expressions:
|
|
|
|
|
return (
|
|
|
|
|
"Le rapport écarte explicitement ce territoire : "
|
|
|
|
|
+ ", ".join(f"« {e} »" for e in self.exclusions[:3])
|
|
|
|
|
)
|
2026-07-25 19:43:03 -04:00
|
|
|
if not self.expressions:
|
|
|
|
|
return None
|
2026-07-25 22:34:22 -04:00
|
|
|
|
|
|
|
|
note = "Détecté d'après : " + ", ".join(self.expressions[:6])
|
|
|
|
|
if self.exclusions:
|
|
|
|
|
note += " — mais le rapport écarte par ailleurs : " + ", ".join(
|
|
|
|
|
f"« {e} »" for e in self.exclusions[:2]
|
|
|
|
|
)
|
|
|
|
|
return note
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
|
|
|
def _configuration() -> dict:
|
|
|
|
|
return yaml.safe_load(chemins.CONFIG.read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
|
|
|
def _motifs() -> list[tuple[re.Pattern[str], str, int]]:
|
|
|
|
|
"""Compile un motif par expression, ancré sur des frontières de mot."""
|
|
|
|
|
expressions = _configuration()["guadeloupe"]["expressions"]
|
|
|
|
|
compiles: list[tuple[re.Pattern[str], str, int]] = []
|
|
|
|
|
|
|
|
|
|
for expression, poids in expressions.items():
|
|
|
|
|
plat = sans_accents(str(expression)).lower()
|
|
|
|
|
compiles.append((re.compile(rf"(?<!\w){re.escape(plat)}\w{{0,3}}(?!\w)"), plat, int(poids)))
|
|
|
|
|
|
|
|
|
|
# Les expressions les plus lourdes d'abord : le compte-rendu cite en
|
|
|
|
|
# priorité ce qui a réellement décidé de la cotation.
|
|
|
|
|
compiles.sort(key=lambda triplet: -triplet[2])
|
|
|
|
|
return compiles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def coter(*fragments: str | None) -> Cotation:
|
2026-07-25 22:34:22 -04:00
|
|
|
"""Cote la pertinence guadeloupéenne d'un ensemble de fragments de texte.
|
|
|
|
|
|
|
|
|
|
Une mention niée ne compte pas. « Sans portée pour la Guadeloupe » nomme le
|
|
|
|
|
territoire pour l'écarter : la lire comme une pertinence forte inverserait
|
|
|
|
|
le sens de la source.
|
|
|
|
|
"""
|
2026-07-25 19:43:03 -04:00
|
|
|
plat = sans_accents(" ".join(f for f in fragments if f)).lower()
|
|
|
|
|
if not plat.strip():
|
|
|
|
|
return Cotation(pertinence=None, score=0, expressions=[])
|
|
|
|
|
|
|
|
|
|
score = 0
|
|
|
|
|
trouvees: list[str] = []
|
2026-07-25 22:34:22 -04:00
|
|
|
exclusions: list[str] = []
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
for motif, expression, poids in _motifs():
|
2026-07-25 22:34:22 -04:00
|
|
|
positions = [m.start() for m in motif.finditer(plat)]
|
|
|
|
|
if not positions:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
retenues = [p for p in positions if not _est_niee(plat, p)]
|
|
|
|
|
if retenues:
|
2026-07-25 19:43:03 -04:00
|
|
|
score += poids
|
|
|
|
|
trouvees.append(expression)
|
2026-07-25 22:34:22 -04:00
|
|
|
else:
|
|
|
|
|
exclusions.append(_extrait_autour(plat, positions[0]))
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
seuils = _configuration()["guadeloupe"]["seuils"]
|
|
|
|
|
if score >= seuils["forte"]:
|
|
|
|
|
pertinence = Pertinence.FORTE
|
|
|
|
|
elif score >= seuils["moyenne"]:
|
|
|
|
|
pertinence = Pertinence.MOYENNE
|
|
|
|
|
elif score >= seuils["faible"]:
|
|
|
|
|
pertinence = Pertinence.FAIBLE
|
2026-07-25 22:34:22 -04:00
|
|
|
elif exclusions:
|
|
|
|
|
# Le territoire est nommé, mais pour être écarté : la pertinence est
|
|
|
|
|
# faible, et le motif dit pourquoi.
|
|
|
|
|
pertinence = Pertinence.FAIBLE
|
2026-07-25 19:43:03 -04:00
|
|
|
else:
|
|
|
|
|
pertinence = None
|
|
|
|
|
|
2026-07-25 22:34:22 -04:00
|
|
|
return Cotation(
|
|
|
|
|
pertinence=pertinence, score=score, expressions=trouvees, exclusions=exclusions
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _est_niee(plat: str, position: int) -> bool:
|
|
|
|
|
"""Vrai si une tournure d'exclusion entoure la mention trouvée."""
|
|
|
|
|
voisinage = plat[max(0, position - _AVANT) : position + _APRES]
|
|
|
|
|
return any(negation in voisinage for negation in NEGATIONS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extrait_autour(plat: str, position: int, largeur: int = 70) -> str:
|
|
|
|
|
debut = max(0, position - largeur)
|
|
|
|
|
fin = min(len(plat), position + largeur)
|
|
|
|
|
return " ".join(plat[debut:fin].split())
|
2026-07-25 19:43:03 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
MOTIF_VERIFICATION = (
|
|
|
|
|
"Pertinence Guadeloupe déduite automatiquement : l'annexe du rapport ne "
|
|
|
|
|
"cote que les lois promulguées, pas les textes encore en discussion."
|
|
|
|
|
)
|