"""App Flask — viewer CIM-10 T2A.""" from __future__ import annotations import json import logging from pathlib import Path import requests from flask import Flask, abort, render_template, request, jsonify from markupsafe import Markup from ..config import STRUCTURED_DIR, OLLAMA_URL, DossierMedical from .. import config as cfg logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def scan_dossiers() -> dict[str, list[dict]]: """Scanne output/structured/ et retourne les fichiers groupés par sous-dossier. Returns: {"racine": [{name, path_rel, dossier}, ...], "sous-dossier": [...]} """ groups: dict[str, list[dict]] = {} for json_path in sorted(STRUCTURED_DIR.rglob("*.json")): rel = json_path.relative_to(STRUCTURED_DIR) parts = rel.parts if len(parts) == 1: group_name = "racine" else: group_name = str(Path(*parts[:-1])) try: data = json.loads(json_path.read_text(encoding="utf-8")) dossier = DossierMedical.model_validate(data) except Exception: logger.warning("Impossible de charger %s", json_path) continue groups.setdefault(group_name, []).append({ "name": json_path.stem, "path_rel": str(rel), "dossier": dossier, }) return groups def load_dossier(path_rel: str) -> DossierMedical: """Charge un JSON et le désérialise. Vérifie contre le path traversal.""" safe_path = (STRUCTURED_DIR / path_rel).resolve() if not safe_path.is_relative_to(STRUCTURED_DIR.resolve()): abort(403) if not safe_path.exists(): abort(404) data = json.loads(safe_path.read_text(encoding="utf-8")) return DossierMedical.model_validate(data) def fetch_ollama_models() -> list[str]: """Appelle GET {OLLAMA_URL}/api/tags pour lister les modèles disponibles.""" try: resp = requests.get(f"{cfg.OLLAMA_URL}/api/tags", timeout=5) resp.raise_for_status() models = resp.json().get("models", []) return [m["name"] for m in models] except Exception: logger.warning("Impossible de contacter Ollama pour lister les modèles") return [] # --------------------------------------------------------------------------- # Filtres Jinja2 # --------------------------------------------------------------------------- _CONFIDENCE_COLORS = { "high": ("#16a34a", "#dcfce7"), "medium": ("#ca8a04", "#fef9c3"), "low": ("#dc2626", "#fee2e2"), } _CONFIDENCE_LABELS = { "high": "Haute", "medium": "Moyenne", "low": "Basse", } def confidence_badge(value: str | None) -> Markup: if not value: return Markup("") fg, bg = _CONFIDENCE_COLORS.get(value, ("#6b7280", "#f3f4f6")) label = _CONFIDENCE_LABELS.get(value, value) return Markup( f'' f'{label}' ) def confidence_label(value: str | None) -> str: if not value: return "" return _CONFIDENCE_LABELS.get(value, value) # --------------------------------------------------------------------------- # App factory # --------------------------------------------------------------------------- def create_app() -> Flask: app = Flask(__name__) app.jinja_env.filters["confidence_badge"] = confidence_badge app.jinja_env.filters["confidence_label"] = confidence_label @app.route("/") def index(): groups = scan_dossiers() return render_template("index.html", groups=groups) @app.route("/dossier/") def detail(filepath: str): dossier = load_dossier(filepath) return render_template("detail.html", dossier=dossier, filepath=filepath) @app.route("/admin/models", methods=["GET"]) def list_models(): models = fetch_ollama_models() return jsonify({"models": models, "current": cfg.OLLAMA_MODEL}) @app.route("/admin/models", methods=["POST"]) def set_model(): data = request.get_json(silent=True) or {} new_model = data.get("model", "").strip() if not new_model: return jsonify({"error": "Champ 'model' requis"}), 400 cfg.OLLAMA_MODEL = new_model logger.info("Modèle Ollama changé : %s", new_model) return jsonify({"ok": True, "model": cfg.OLLAMA_MODEL}) @app.route("/reprocess/", methods=["POST"]) def reprocess(filepath: str): """Relance le traitement d'un dossier.""" from ..main import process_pdf, write_outputs dossier = load_dossier(filepath) source_file = dossier.source_file if not source_file: return jsonify({"error": "Fichier source introuvable"}), 400 # Chercher le PDF source dans input/ input_dir = Path(__file__).parent.parent.parent / "input" pdf_path = None for p in input_dir.rglob(source_file): if p.is_file(): pdf_path = p break if not pdf_path: return jsonify({"error": f"PDF source '{source_file}' introuvable"}), 404 try: anonymized_text, new_dossier, report = process_pdf(pdf_path) stem = pdf_path.stem.replace(" ", "_") subdir = None if pdf_path.parent != input_dir: subdir = pdf_path.parent.name write_outputs(stem, anonymized_text, new_dossier, report, subdir=subdir) return jsonify({"ok": True, "message": "Traitement terminé"}) except Exception as e: logger.exception("Erreur lors du retraitement") return jsonify({"error": str(e)}), 500 return app