feat: ajout viewer Flask CIM-10 avec config Ollama centralisée et chronométrage
Ajoute une interface web Flask pour visualiser les dossiers médicaux CIM-10, avec temps de traitement par PDF, sélecteur de modèle Ollama, et centralisation de la config Ollama dans src/config.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
150
src/viewer/app.py
Normal file
150
src/viewer/app.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""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'<span style="display:inline-block;padding:2px 8px;border-radius:9999px;'
|
||||
f'font-size:0.75rem;font-weight:600;color:{fg};background:{bg}">'
|
||||
f'{label}</span>'
|
||||
)
|
||||
|
||||
|
||||
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/<path:filepath>")
|
||||
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})
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user