Spaces:
Sleeping
Sleeping
File size: 8,906 Bytes
b80ac6e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Backends d'extraction d'entitΓ©s nommΓ©es (Sprint 40).
Suite directe du Sprint 38 : la couche de calcul (`compute_ner_metrics`)
prend deux listes d'entitΓ©s, ce module fournit le moyen d'**obtenir** la
liste d'entitΓ©s d'un cΓ΄tΓ© Γ partir d'un texte (gΓ©nΓ©ralement la sortie
OCR du moteur).
Architecture
------------
- ``EntityExtractor`` : Protocol Python qui dΓ©crit l'interface ; tout
callable ``(text: str) -> list[dict]`` est un extracteur valide. Le
format de sortie est compatible ``EntitiesGT`` (Sprint 32) et
``compute_ner_metrics`` (Sprint 38).
- ``SpacyEntityExtractor`` : implΓ©mentation par dΓ©faut, lazy-import de
spaCy. Si spaCy n'est pas installé OU si le modèle n'est pas
tΓ©lΓ©chargΓ©, retourne ``[]`` avec un ``logger.warning`` explicite
(cf. règle CLAUDE.md : pas de ``except: pass``).
- ``SPACY_PROFILES`` : dict de profils nommés vers noms de modèles
spaCy (FR, EN, multilingue, HIPE pour les corpus historiques).
- ``get_extractor(profile)`` : factory qui retourne l'extracteur
correspondant au profil demandΓ©.
DΓ©couplage runner β backend
---------------------------
Le runner reΓ§oit un ``EntityExtractor`` en paramΓ¨tre β il n'importe
**jamais** spaCy directement. Cela permet :
1. de **tester** sans dΓ©pendance externe (le test injecte un callable
qui simule l'extraction) ;
2. de **brancher** des backends alternatifs (Stanza, HIPE custom,
modèle fine-tuné maison) sans modifier le runner ;
3. de **dΓ©sactiver** la mΓ©trique en passant ``None`` β comportement
par dΓ©faut, rΓ©trocompat stricte.
"""
from __future__ import annotations
import logging
from typing import Any, Protocol
logger = logging.getLogger(__name__)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Interface
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class EntityExtractor(Protocol):
"""Tout callable ``(text) -> list[dict]`` est un extracteur valide.
Format de sortie attendu : liste de dicts
``{"label": str, "start": int, "end": int, "text": str}``
compatibles avec ``compute_ner_metrics`` (Sprint 38) et
``EntitiesGT`` (Sprint 32).
"""
def __call__(self, text: str) -> list[dict[str, Any]]: ...
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Profils spaCy nommΓ©s
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SPACY_PROFILES: dict[str, str] = {
"fr": "fr_core_news_sm",
"fr_lg": "fr_core_news_lg",
"en": "en_core_web_sm",
"en_lg": "en_core_web_lg",
"multilingual": "xx_ent_wiki_sm",
# HIPE 2022 β modΓ¨le historique multilingue (Hugging Face). Pas
# toujours disponible via ``spacy.load`` direct ; documentΓ© pour
# mΓ©moire, l'utilisateur peut le wrapper dans un EntityExtractor
# custom si besoin.
"hipe": "fr_core_news_lg",
}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Backend spaCy
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class SpacyEntityExtractor:
"""Extracteur d'entitΓ©s basΓ© sur spaCy.
Lazy-import : ``spacy`` n'est importΓ© qu'au premier appel. Le
modèle est chargé une seule fois et mis en cache sur l'instance.
Si spaCy n'est pas installé OU si le modèle demandé n'est pas
tΓ©lΓ©chargΓ©, l'extracteur tombe en mode dΓ©gradΓ© (retourne ``[]``
pour chaque appel) et Γ©met un ``logger.warning`` au premier
appel.
Parameters
----------
model_name:
Nom du modèle spaCy à charger (ex. ``"fr_core_news_sm"``).
label_mapping:
Dict optionnel ``{spacy_label: target_label}`` pour
normaliser les labels (ex. spaCy utilise ``"PERSON"``,
on veut ``"PER"``). Si ``None``, garde les labels tels
quels.
Examples
--------
>>> extractor = SpacyEntityExtractor("fr_core_news_sm")
>>> entities = extractor("Marie de Bourgogne, en 1477.")
>>> # liste de dicts {label, start, end, text}, ou [] si spaCy absent
"""
# Mapping par dΓ©faut spaCy β conventions HIPE/CoNLL courtes
DEFAULT_LABEL_MAPPING: dict[str, str] = {
"PERSON": "PER",
"PER": "PER",
"LOC": "LOC",
"GPE": "LOC", # Geo-Political Entity β LOC
"ORG": "ORG",
"DATE": "DATE",
"TIME": "DATE",
"MISC": "MISC",
}
def __init__(
self,
model_name: str = "fr_core_news_sm",
label_mapping: dict[str, str] | None = None,
) -> None:
self.model_name = model_name
self.label_mapping = (
dict(label_mapping)
if label_mapping is not None
else dict(self.DEFAULT_LABEL_MAPPING)
)
self._nlp: Any | None = None
self._loaded: bool = False
self._available: bool = False
def _load(self) -> None:
"""Charge spaCy + modèle au premier appel. Idempotent."""
if self._loaded:
return
self._loaded = True
try:
import spacy # type: ignore[import-untyped]
except ImportError as exc:
logger.warning(
"[ner_backends] spaCy non installΓ© (%s) β extraction NER "
"dΓ©sactivΓ©e. Installer avec `pip install picarones[ner]`.",
exc,
)
return
try:
self._nlp = spacy.load(self.model_name)
self._available = True
except OSError as exc:
logger.warning(
"[ner_backends] ModΓ¨le spaCy %r introuvable (%s) β extraction "
"NER dΓ©sactivΓ©e. TΓ©lΓ©charger avec `python -m spacy download %s`.",
self.model_name, exc, self.model_name,
)
@property
def available(self) -> bool:
"""``True`` si spaCy + le modèle sont chargés et utilisables."""
if not self._loaded:
self._load()
return self._available
def __call__(self, text: str) -> list[dict[str, Any]]:
if not text:
return []
if not self.available or self._nlp is None:
return []
doc = self._nlp(text)
results: list[dict[str, Any]] = []
for ent in doc.ents:
label = self.label_mapping.get(ent.label_, ent.label_)
results.append({
"label": label,
"start": int(ent.start_char),
"end": int(ent.end_char),
"text": ent.text,
})
return results
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Factory
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_extractor(profile: str = "fr") -> SpacyEntityExtractor:
"""Retourne un extracteur spaCy pour le profil demandΓ©.
Le profil peut Γͺtre :
- une clΓ© de ``SPACY_PROFILES`` (ex. ``"fr"``, ``"en"``,
``"multilingual"``)
- un nom de modèle spaCy direct (ex. ``"fr_core_news_lg"``)
L'extracteur est instancié paresseusement (le modèle n'est chargé
qu'au premier appel). Si le modèle n'est pas disponible,
l'extracteur tombe en mode dΓ©gradΓ© silencieux (retourne ``[]``).
"""
model_name = SPACY_PROFILES.get(profile, profile)
return SpacyEntityExtractor(model_name=model_name)
def is_spacy_available() -> bool:
"""``True`` si la librairie ``spacy`` est importable, sans charger
de modèle."""
try:
import spacy # noqa: F401
except ImportError:
return False
return True
__all__ = [
"EntityExtractor",
"SpacyEntityExtractor",
"SPACY_PROFILES",
"get_extractor",
"is_spacy_available",
]
|