Files
medical_ai_scribe/medical_scribe_gui_v3.py
2026-03-05 01:20:13 +01:00

236 lines
10 KiB
Python

import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import subprocess
import time
import ollama
from fpdf import FPDF
# Token HF
HF_TOKEN = "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"
# Dictionnaire de Prompts Enrichi
PROMPT_TEMPLATES = {
"Contrôle T2A / Codage CIM-10": """Tu es un médecin DIM expert. Analyse cette réunion sur le codage CIM-10 et produis :
1. Un tableau des phases (Avant/Pendant/Après contrôle).
2. Une check-list des points d'attention (DP, DAS, Actes).
3. Un modèle de fiche de synthèse pratique.
Sois extrêmement rigoureux sur la terminologie PMSI/T2A.""",
"Consultation Standard": """Analyse cette consultation médicale et produis une synthèse :
1. Motif et symptômes.
2. Antécédents et Examen clinique.
3. Diagnostic et Plan thérapeutique (ordonnance).""",
"Compte-rendu Opératoire": """Produis un compte-rendu chirurgical structuré :
1. Indication et type d'intervention.
2. Description technique détaillée.
3. Matériel et incidents.
4. Suites prévues.""",
"Lettre au Confrère": """Rédige une lettre de liaison formelle adressée à un confrère à partir de ce transcript.
Format professionnel avec en-tête et conclusion standard.""",
"Prompt Personnalisé": "Tapez vos propres instructions ici..."
}
class PDF(FPDF):
def header(self):
self.set_font('Arial', 'B', 15)
self.cell(0, 10, 'Compte-rendu Médical IA', 0, 1, 'C')
self.ln(5)
class MedicalScribeGUIv3:
def __init__(self, root):
self.root = root
self.root.title("Medical AI Scribe v3.0 - Expert Edition")
self.root.geometry("1000x900")
self.root.configure(bg="#eceff1")
# Variables
self.audio_path = tk.StringVar()
self.selected_model = tk.StringVar()
self.selected_template = tk.StringVar(value="Contrôle T2A / Codage CIM-10")
self.status_var = tk.StringVar(value="Prêt.")
self.progress_val = tk.DoubleVar(value=0)
self.last_summary_path = None
self.current_process = None
self.setup_ui()
threading.Thread(target=self.load_ollama_models, daemon=True).start()
def setup_ui(self):
main_frame = tk.Frame(self.root, bg="#eceff1", padx=25, pady=25)
main_frame.pack(fill=tk.BOTH, expand=True)
# Titre
tk.Label(main_frame, text="MEDICAL AI SCRIBE EXPERT", font=("Segoe UI", 20, "bold"), bg="#eceff1", fg="#263238").pack(pady=(0, 20))
# Zone Fichier
f_frame = tk.LabelFrame(main_frame, text=" 1. Source Audio ", font=("Arial", 10, "bold"), bg="#eceff1", padx=10, pady=10)
f_frame.pack(fill="x", pady=10)
tk.Entry(f_frame, textvariable=self.audio_path, font=("Arial", 10), width=90).pack(side="left", padx=5)
tk.Button(f_frame, text="Parcourir", command=self.browse_file, bg="#607d8b", fg="white").pack(side="left", padx=5)
# Zone Configuration
c_frame = tk.LabelFrame(main_frame, text=" 2. Intelligence & Format ", font=("Arial", 10, "bold"), bg="#eceff1", padx=10, pady=10)
c_frame.pack(fill="x", pady=10)
tk.Label(c_frame, text="Modèle Ollama:", bg="#eceff1").grid(row=0, column=0, sticky="w")
self.model_combo = ttk.Combobox(c_frame, textvariable=self.selected_model, width=40)
self.model_combo.grid(row=0, column=1, padx=10, pady=5, sticky="w")
self.model_combo.set("gpt-oss:120b-cloud")
tk.Label(c_frame, text="Type de document:", bg="#eceff1").grid(row=1, column=0, sticky="w")
self.template_combo = ttk.Combobox(c_frame, textvariable=self.selected_template, width=40, state="readonly")
self.template_combo['values'] = list(PROMPT_TEMPLATES.keys())
self.template_combo.grid(row=1, column=1, padx=10, pady=5, sticky="w")
self.template_combo.bind("<<ComboboxSelected>>", self.on_template_change)
# Prompt
tk.Label(main_frame, text="Instructions de synthèse:", font=("Arial", 10, "bold"), bg="#eceff1").pack(anchor="w", pady=(10, 0))
self.prompt_text = tk.Text(main_frame, height=6, font=("Arial", 10), padx=10, pady=10)
self.prompt_text.pack(fill="x", pady=5)
self.prompt_text.insert("1.0", PROMPT_TEMPLATES["Contrôle T2A / Codage CIM-10"])
# Progression
tk.Label(main_frame, text="Progression du traitement:", font=("Arial", 9), bg="#eceff1").pack(anchor="w", pady=(10, 0))
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_val, maximum=100, mode='determinate')
self.progress_bar.pack(fill="x", pady=5)
self.status_label = tk.Label(main_frame, textvariable=self.status_var, font=("Arial", 10, "bold"), bg="#eceff1", fg="#1565c0")
self.status_label.pack()
# Boutons
btn_frame = tk.Frame(main_frame, bg="#eceff1")
btn_frame.pack(fill="x", pady=20)
self.run_btn = tk.Button(btn_frame, text="LANCER LE PIPELINE", bg="#2e7d32", fg="white", font=("Arial", 12, "bold"),
width=25, pady=10, command=self.start_pipeline)
self.run_btn.pack(side="left", padx=5)
self.stop_btn = tk.Button(btn_frame, text="STOP", bg="#c62828", fg="white", font=("Arial", 12, "bold"),
width=10, pady=10, state="disabled", command=self.stop_pipeline)
self.stop_btn.pack(side="left", padx=5)
self.pdf_btn = tk.Button(btn_frame, text="EXPORTER PDF", bg="#f57c00", fg="white", font=("Arial", 12, "bold"),
width=15, pady=10, state="disabled", command=self.export_pdf)
self.pdf_btn.pack(side="right", padx=5)
# Logs
self.log_area = tk.Text(main_frame, height=12, bg="#212121", fg="#76ff03", font=("Consolas", 9), padx=10, pady=10)
self.log_area.pack(fill=tk.BOTH, expand=True)
def on_template_change(self, event):
self.prompt_text.delete("1.0", tk.END)
self.prompt_text.insert("1.0", PROMPT_TEMPLATES[self.selected_template.get()])
def browse_file(self):
fn = filedialog.askopenfilename(filetypes=[("Audio", "*.wav *.mp3 *.m4a *.flac")])
if fn: self.audio_path.set(fn)
def load_ollama_models(self):
try:
resp = ollama.list()
models = getattr(resp, 'models', []) if hasattr(resp, 'models') else resp.get('models', [])
names = [m.model if hasattr(m, 'model') else m.get('model', 'Unknown') for m in models]
if names: self.model_combo['values'] = names
except: pass
def log(self, msg):
self.log_area.insert(tk.END, str(msg) + "\n")
self.log_area.see(tk.END)
self.root.update_idletasks()
def start_pipeline(self):
if not self.audio_path.get(): return
self.run_btn.config(state="disabled")
self.stop_btn.config(state="normal")
self.pdf_btn.config(state="disabled")
self.log_area.delete("1.0", tk.END)
threading.Thread(target=self.run_worker, daemon=True).start()
def run_worker(self):
audio = self.audio_path.get()
model_name = self.selected_model.get()
prompt = self.prompt_text.get("1.0", tk.END).strip()
try:
self.log("--- DÉMARRAGE DU MOTEUR EXPERT ---")
script_dir = os.path.dirname(os.path.abspath(__file__))
env = os.environ.copy()
env["HF_TOKEN"] = HF_TOKEN
self.current_process = subprocess.Popen(
[sys.executable, os.path.join(script_dir, "medical_diarizer.py"), audio],
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in self.current_process.stdout:
line = line.strip()
if line.startswith("[STATUS] PROGRESS:"):
try:
val = int(line.split(":")[1])
self.progress_val.set(val)
self.status_var.set(f"Analyse en cours : {val}%")
except: pass
else:
self.log(line)
self.current_process.wait()
transcript_file = audio.rsplit('.', 1)[0] + "_diarized.txt"
if not os.path.exists(transcript_file): raise Exception("Échec moteur.")
self.status_var.set("Génération de la synthèse IA...")
self.log("\n--- GÉNÉRATION DE LA SYNTHÈSE MÉDICALE ---")
import ollama
with open(transcript_file, "r") as f: content = f.read()
resp = ollama.chat(model=model_name, messages=[
{"role": "system", "content": prompt}, {"role": "user", "content": content}
])
self.last_summary_path = audio.rsplit('.', 1)[0] + "_final_summary.md"
with open(self.last_summary_path, "w") as f: f.write(resp['message']['content'])
self.log("\n[OK] Synthèse générée avec succès.")
self.status_var.set("Terminé. Prêt pour export PDF.")
self.pdf_btn.config(state="normal")
messagebox.showinfo("Succès", "Traitement terminé !")
except Exception as e:
self.log(f"\n[ERREUR] {e}")
finally:
self.run_btn.config(state="normal")
self.stop_btn.config(state="disabled")
def stop_pipeline(self):
if self.current_process:
self.current_process.terminate()
self.log("\n[STOP] Abandon.")
self.status_var.set("Prêt.")
def export_pdf(self):
if not self.last_summary_path or not os.path.exists(self.last_summary_path): return
try:
pdf = PDF()
pdf.add_page()
pdf.set_font("Arial", size=11)
with open(self.last_summary_path, 'r', encoding='utf-8') as f:
for line in f:
clean_line = line.encode('latin-1', 'replace').decode('latin-1')
pdf.multi_cell(0, 10, clean_line)
pdf_path = self.last_summary_path.replace(".md", ".pdf")
pdf.output(pdf_path)
os.system(f"xdg-open '{pdf_path}'")
messagebox.showinfo("PDF", f"PDF généré et ouvert :\n{os.path.basename(pdf_path)}")
except Exception as e:
messagebox.showerror("Erreur PDF", str(e))
if __name__ == "__main__":
root = tk.Tk()
MedicalScribeGUIv3(root)
root.mainloop()