"""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