Spaces:
Sleeping
Sleeping
Claude
fix(web): durcir le parsing XML (defusedxml en dépendance dure) + exceptions précises
de46be0 unverified | """Helpers métier des routeurs ``engines`` et ``models``. | |
| Ces utilitaires détectent la disponibilité des moteurs OCR et LLM | |
| locaux, listent leurs modèles, et inférent leurs capacités (text / | |
| vision) à partir du nom — quand l'API du provider ne fournit pas | |
| cette information directement. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| # ────────────────────────────────────────────────────────────────────────── | |
| # Tables de capacités par famille | |
| # ────────────────────────────────────────────────────────────────────────── | |
| MISTRAL_TEXT_ONLY = frozenset({ | |
| "ministral-3b-latest", "ministral-8b-latest", "mistral-tiny", | |
| "mistral-tiny-latest", "open-mistral-7b", "open-mixtral-8x7b", | |
| "mistral-small-latest", "mistral-small-2409", | |
| }) | |
| """Modèles Mistral explicitement text-only (pas de support vision).""" | |
| MISTRAL_TEXT_ONLY_PREFIXES = ( | |
| "ministral", "open-mistral", "open-mixtral", "codestral", | |
| "mistral-embed", "mistral-tiny", | |
| ) | |
| """Préfixes de modèles Mistral à traiter comme text-only.""" | |
| OLLAMA_VISION_FAMILIES = frozenset({ | |
| "llava", "bakllava", "moondream", "minicpm-v", "llama3.2-vision", | |
| "llava-llama3", "llava-phi3", "nanollava", | |
| }) | |
| """Familles Ollama multimodales connues.""" | |
| # ────────────────────────────────────────────────────────────────────────── | |
| # Disponibilité des moteurs locaux | |
| # ────────────────────────────────────────────────────────────────────────── | |
| def check_engine(engine_id: str, module_name: str, label: str = "") -> dict: | |
| """Vérifie qu'un moteur OCR local est installé et retourne son statut. | |
| On ne fait que ``__import__(module_name)`` + ``getattr(mod, "__version__")`` — | |
| seules ``ImportError`` et ``AttributeError`` peuvent légitimement | |
| survenir. Tout autre type d'exception (panne disque, OSError…) est | |
| propagé pour ne pas masquer un vrai bug. | |
| """ | |
| label = label or engine_id.replace("_", " ").title() | |
| try: | |
| __import__(module_name) | |
| installed = True | |
| except ImportError: | |
| installed = False | |
| version = "" | |
| if installed and engine_id == "tesseract": | |
| try: | |
| import pytesseract | |
| version = str(pytesseract.get_tesseract_version()) | |
| except (ImportError, pytesseract.TesseractNotFoundError, OSError): | |
| # ``TesseractNotFoundError`` : binaire absent ; ``OSError`` : | |
| # ``PATH`` manquant ; ``ImportError`` : racine du sous-import. | |
| version = "installé" | |
| elif installed: | |
| try: | |
| mod = __import__(module_name) | |
| version = getattr(mod, "__version__", "installé") | |
| except (ImportError, AttributeError): | |
| version = "installé" | |
| return { | |
| "id": engine_id, | |
| "label": label, | |
| "type": "ocr", | |
| "available": installed, | |
| "version": version, | |
| "status": "available" if installed else "not_installed", | |
| } | |
| def fetch_ollama_info() -> tuple[bool, list[str]]: | |
| """Vérifie la disponibilité d'Ollama et liste ses modèles en un seul appel HTTP. | |
| Capture explicitement ``URLError`` (Ollama pas démarré, port fermé, | |
| timeout) et ``json.JSONDecodeError`` (réponse non-JSON inattendue). | |
| Toute autre exception (par ex. ``OSError`` sur lecture réseau, | |
| ``UnicodeDecodeError``) est aussi traitée comme "Ollama | |
| indisponible" — c'est l'intention du caller (UX dégradée gracieuse). | |
| """ | |
| import urllib.error | |
| import urllib.request | |
| try: | |
| with urllib.request.urlopen("http://localhost:11434/api/tags", timeout=2) as r: | |
| if r.status != 200: | |
| return False, [] | |
| data = json.loads(r.read().decode()) | |
| except (urllib.error.URLError, OSError, json.JSONDecodeError, UnicodeDecodeError): | |
| return False, [] | |
| models = [m.get("name", "") for m in data.get("models", [])] | |
| return True, models | |
| def get_tesseract_langs() -> list[str]: | |
| """Liste les langues Tesseract installées (avec fallback éditorial). | |
| ``TesseractNotFoundError`` quand le binaire est absent du ``PATH``, | |
| ``ImportError`` si pytesseract n'est pas installé, ``OSError`` sur | |
| appel système échoué — tous traités comme "lister les langues | |
| indisponible, fallback à la liste éditoriale historique". | |
| """ | |
| try: | |
| import pytesseract | |
| langs = pytesseract.get_languages(config="") | |
| return sorted(lg for lg in langs if lg != "osd") | |
| except (ImportError, OSError): | |
| # ``pytesseract.TesseractNotFoundError`` hérite d'``OSError``. | |
| return ["fra", "lat", "eng", "deu", "ita", "spa"] | |
| # ────────────────────────────────────────────────────────────────────────── | |
| # Inférence de capacités par nom de modèle | |
| # ────────────────────────────────────────────────────────────────────────── | |
| def model_entry(model_id: str, capabilities: list[str]) -> dict: | |
| """Crée une entrée modèle avec son ID et ses capacités.""" | |
| return {"id": model_id, "capabilities": capabilities} | |
| def infer_mistral_capabilities(model_id: str) -> list[str]: | |
| mid = model_id.lower() | |
| # Modèles explicitement vision (Pixtral) | |
| if "pixtral" in mid: | |
| return ["text", "vision"] | |
| # Modèles explicitement text-only | |
| if mid in MISTRAL_TEXT_ONLY or any(mid.startswith(p) for p in MISTRAL_TEXT_ONLY_PREFIXES): | |
| return ["text"] | |
| # Mistral Large et modèles récents non-identifiés → vision par défaut | |
| if "mistral-large" in mid or "mistral-medium" in mid: | |
| return ["text", "vision"] | |
| # Par défaut, marquer comme text-only (plus sûr que de supposer vision) | |
| return ["text"] | |
| def infer_openai_capabilities(model_id: str) -> list[str]: | |
| mid = model_id.lower() | |
| if "gpt-4o" in mid or "gpt-4-turbo" in mid or "gpt-4.1" in mid or "o1" in mid or "o3" in mid: | |
| return ["text", "vision"] | |
| return ["text"] | |
| def infer_ollama_capabilities(model_name: str) -> list[str]: | |
| base = model_name.split(":")[0].lower() | |
| if any(base.startswith(family) for family in OLLAMA_VISION_FAMILIES): | |
| return ["text", "vision"] | |
| return ["text"] | |
| __all__ = [ | |
| "MISTRAL_TEXT_ONLY", | |
| "MISTRAL_TEXT_ONLY_PREFIXES", | |
| "OLLAMA_VISION_FAMILIES", | |
| "check_engine", | |
| "fetch_ollama_info", | |
| "get_tesseract_langs", | |
| "model_entry", | |
| "infer_mistral_capabilities", | |
| "infer_openai_capabilities", | |
| "infer_ollama_capabilities", | |
| ] | |