"""Détient un `LocalePipeline` par locale supportée — le seul état mutable partagé du process (une instance vit pour toute la durée de vie d'`uvicorn`, montée sur `app.state`, voir `main.py`). Volontairement une classe "registre" séparée de `LocalePipeline` lui-même : `LocalePipeline` ne connaît qu'une seule locale, ce module route `train`/`process` vers la bonne instance selon le `locale` reçu dans la requête — même séparation de responsabilité que `TechStepClassifierService` (une seule instance, un seul `NlpManager` multi-langues) avait implicitement via node-nlp, explicitée ici puisque spaCy charge un modèle par langue. """ from .locale_pipeline import SUPPORTED_LOCALES, LocalePipeline, ProcessResult, TrainEntry, UnsupportedLocaleError class PipelineRegistry: def __init__(self) -> None: self._pipelines: dict[str, LocalePipeline] = { locale: LocalePipeline(locale) for locale in SUPPORTED_LOCALES } def preload_all(self) -> None: """Charge le modèle spaCy de base de chaque locale connue — appelé une fois au démarrage du process (`main.py`), pas paresseusement au premier appel, pour que `GET /health` ne réponde `200` qu'une fois ce coût payé (voir `LocalePipeline.preload`).""" for pipeline in self._pipelines.values(): pipeline.preload() def train(self, locale: str, entries: list[TrainEntry]) -> tuple[int, int, int]: pipeline = self._pipelines.get(locale) if pipeline is None: raise UnsupportedLocaleError(f"Unsupported locale: {locale!r}") return pipeline.train(entries) def process(self, locale: str, text: str) -> ProcessResult: pipeline = self._pipelines.get(locale) if pipeline is None: # Une locale que ce service ne sait structurellement pas # charger (pas de modèle spaCy connu) se comporte comme une # locale "jamais entraînée" côté `process` — reproduit le test # `apps/api` existant ("returns an empty sequence for a locale # nothing was trained on"), qui ne distingue pas les deux cas. return ProcessResult(entities=[], intent=None, score=0.0) return pipeline.process(text) registry = PipelineRegistry()