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("<>", 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()