diff --git a/gui_v6/tabs/tab_config.py b/gui_v6/tabs/tab_config.py index 4c2088f..a8d484d 100644 --- a/gui_v6/tabs/tab_config.py +++ b/gui_v6/tabs/tab_config.py @@ -15,6 +15,7 @@ from tkinter import filedialog, messagebox import customtkinter as ctk +import engine_capabilities from gui_v6 import theme as theme_mod from gui_v6 import ui_kit from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys @@ -305,16 +306,26 @@ class ConfigTab(ctk.CTkFrame): hint_row, text="Pourquoi pas tout coché ?", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w" ).pack(side="left") ui_kit.help_button(hint_row, p, _HELP_MOTEURS, title="Moteurs de détection").pack(side="right") + # Honnêteté moteurs : ne pas proposer un moteur que ce build n'embarque pas. + caps = engine_capabilities.capabilities_map() + eds_off = not caps["eds"].available + gli_off = not caps["gliner"].available + if eds_off: + self._state.enable_eds = False + if gli_off: + self._state.enable_gliner = False self._tog_ner = self._mini_toggle( ner, "CamemBERT-bio", "standard · rapide · F1 0.963", value=self._state.use_local_ner, command=self._on_ner ) self._tog_ner.pack(fill="x", padx=12, pady=1) self._tog_eds = self._mini_toggle( - ner, "EDS-Pseudo", "optionnel · médical français · plus lent", value=self._state.enable_eds, command=self._on_eds + ner, "EDS-Pseudo", "optionnel · médical français · plus lent", value=self._state.enable_eds, + command=self._on_eds, disabled=eds_off, disabled_hint="non embarqué dans cette version", ) self._tog_eds.pack(fill="x", padx=12, pady=1) self._tog_gli = self._mini_toggle( - ner, "GLiNER", "optionnel · vote croisé · plus lent", value=self._state.enable_gliner, command=self._on_gliner + ner, "GLiNER", "optionnel · vote croisé · plus lent", value=self._state.enable_gliner, + command=self._on_gliner, disabled=gli_off, disabled_hint="non embarqué dans cette version", ) self._tog_gli.pack(fill="x", padx=12, pady=1) self._mini_toggle( @@ -512,7 +523,14 @@ class ConfigTab(ctk.CTkFrame): self._pro_disable_vlm_var = ctk.BooleanVar(value=False) self._pro_vlm_switch = ctk.CTkSwitch(eng, text="Désactiver le moteur VLM (images)", variable=self._pro_disable_vlm_var, progress_color=p["primary"], text_color=p["text"], font=ui_kit.font(12)) self._pro_vlm_switch.pack(anchor="w", padx=12, pady=(0, 6)) - ctk.CTkLabel(eng, text="CamemBERT-bio (standard) toujours actif ; EDS-Pseudo / GLiNER optionnels.", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w", wraplength=300, justify="left").pack(fill="x", padx=12, pady=(0, 12)) + # Note honnête : reflète les moteurs réellement embarqués par ce build. + caps_pro = engine_capabilities.capabilities_map() + opt = [c.label.split(" (")[0] for c in (caps_pro["eds"], caps_pro["gliner"]) if c.available] + if opt: + moteurs_note = "CamemBERT-bio (standard) toujours actif ; " + " / ".join(opt) + " disponibles (optionnels)." + else: + moteurs_note = "CamemBERT-bio (standard) toujours actif ; EDS-Pseudo / GLiNER non embarqués dans cette version." + ctk.CTkLabel(eng, text=moteurs_note, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w", wraplength=300, justify="left").pack(fill="x", padx=12, pady=(0, 12)) words = ui_kit.Card(right, p, title="📝 Mots du profil") words.pack(fill="both", expand=True) @@ -941,19 +959,30 @@ class ConfigTab(ctk.CTkFrame): wraplength=330, ).pack(fill="x", padx=12, pady=(0, 10), ipady=5) - def _mini_toggle(self, parent, label: str, hint: str, value: bool = True, variable=None, command=None): + def _mini_toggle(self, parent, label: str, hint: str, value: bool = True, variable=None, + command=None, disabled: bool = False, disabled_hint: str | None = None): p = self._p row = ctk.CTkFrame(parent, fg_color="transparent", height=34) row.pack_propagate(False) left = ctk.CTkFrame(row, fg_color="transparent") left.pack(side="left", fill="x", expand=True) - ctk.CTkLabel(left, text=label, text_color=p["text"], font=ui_kit.font(12), anchor="w").pack(anchor="w") - if hint: - ctk.CTkLabel(left, text=hint, text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w") + lbl_color = p["text_muted"] if disabled else p["text"] + ctk.CTkLabel(left, text=label, text_color=lbl_color, font=ui_kit.font(12), anchor="w").pack(anchor="w") + shown_hint = disabled_hint if (disabled and disabled_hint) else hint + if shown_hint: + ctk.CTkLabel(left, text=shown_hint, text_color=p["text_muted"], font=ui_kit.font(10), anchor="w").pack(anchor="w") + # Moteur indisponible : on force l'état à False (jamais « coché mais absent »). + if disabled and variable is None: + value = False var = variable if variable is not None else ctk.BooleanVar(value=value) + if disabled: + var.set(False) switch = ctk.CTkSwitch(row, text="", variable=var, command=command, progress_color=p["primary"], width=38) + if disabled: + switch.configure(state="disabled") switch.pack(side="right", padx=(6, 0)) row.var = var # type: ignore[attr-defined] + row.switch = switch # type: ignore[attr-defined] row.get = lambda: bool(var.get()) # type: ignore[attr-defined] return row diff --git a/tests/unit/test_gui_v6_profiles.py b/tests/unit/test_gui_v6_profiles.py index 1da9c54..6083942 100644 --- a/tests/unit/test_gui_v6_profiles.py +++ b/tests/unit/test_gui_v6_profiles.py @@ -295,3 +295,48 @@ def test_regles_moved_into_profils(ctk_root, tmp_path, monkeypatch): # le builder du sous-onglet séparé n'existe plus assert "rul" not in tab._panels tab.destroy() + + +def test_unavailable_engines_disabled_in_reglages(ctk_root, tmp_path, monkeypatch): + """Honnêteté moteurs : EDS-Pseudo / GLiNER non embarqués → switch désactivé + et état forcé à False ; CamemBERT-bio reste actif.""" + import engine_capabilities as ec + from gui_v6.tabs import tab_config + + monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path) + fake = { + "camembert": ec.EngineCapability("camembert", "CamemBERT-bio (standard)", True, True, "ok"), + "eds": ec.EngineCapability("eds", "EDS-Pseudo (optionnel)", False, False, "non embarqué dans cette version (manque : edsnlp, spacy)"), + "gliner": ec.EngineCapability("gliner", "GLiNER (optionnel)", False, False, "non embarqué dans cette version (manque : gliner)"), + } + monkeypatch.setattr(tab_config.engine_capabilities, "capabilities_map", lambda probes=None: fake) + + tab = tab_config.ConfigTab(ctk_root) + tab.update_idletasks() + + assert str(tab._tog_ner.switch.cget("state")) == "normal" # CamemBERT standard actif + assert str(tab._tog_eds.switch.cget("state")) == "disabled" + assert str(tab._tog_gli.switch.cget("state")) == "disabled" + assert tab._state.enable_eds is False + assert tab._state.enable_gliner is False + tab.destroy() + + +def test_available_engines_enabled_in_reglages(ctk_root, tmp_path, monkeypatch): + """Si les moteurs optionnels sont embarqués, leurs switches restent actifs.""" + import engine_capabilities as ec + from gui_v6.tabs import tab_config + + monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path) + fake = { + "camembert": ec.EngineCapability("camembert", "CamemBERT-bio (standard)", True, True, "ok"), + "eds": ec.EngineCapability("eds", "EDS-Pseudo (optionnel)", True, False, "edsnlp + spacy disponibles"), + "gliner": ec.EngineCapability("gliner", "GLiNER (optionnel)", True, False, "gliner disponible"), + } + monkeypatch.setattr(tab_config.engine_capabilities, "capabilities_map", lambda probes=None: fake) + + tab = tab_config.ConfigTab(ctk_root) + tab.update_idletasks() + assert str(tab._tog_eds.switch.cget("state")) == "normal" + assert str(tab._tog_gli.switch.cget("state")) == "normal" + tab.destroy()