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

202 lines
8.8 KiB
Python

import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import subprocess
import time
# Token HF en dur (depuis votre message)
HF_TOKEN = "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"
class MedicalScribeGUI:
def __init__(self, root):
self.root = root
self.root.title("Medical AI Scribe - v1.0")
self.root.geometry("850x750")
self.root.configure(bg="#f8f9fa")
# Variables
self.audio_path = tk.StringVar()
self.selected_model = tk.StringVar()
self.status_var = tk.StringVar(value="Système prêt.")
self.setup_ui()
# Charger les modèles Ollama au démarrage de manière asynchrone
threading.Thread(target=self.load_ollama_models, daemon=True).start()
def setup_ui(self):
main_frame = tk.Frame(self.root, bg="#f8f9fa", padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 1. Sélection du fichier
tk.Label(main_frame, text="Fichier Audio :", bg="#f8f9fa", font=("Arial", 10, "bold")).pack(anchor="w")
file_frame = tk.Frame(main_frame, bg="#f8f9fa")
file_frame.pack(fill="x", pady=(5, 15))
tk.Entry(file_frame, textvariable=self.audio_path, font=("Arial", 10), width=80).pack(side="left", padx=(0, 10))
tk.Button(file_frame, text="Parcourir", command=self.browse_file, bg="#dee2e6").pack(side="left")
# 2. Sélection du modèle
tk.Label(main_frame, text="Modèle Ollama pour la synthèse :", bg="#f8f9fa", font=("Arial", 10, "bold")).pack(anchor="w")
# On permet d'écrire le nom du modèle s'il n'est pas dans la liste
self.model_combo = ttk.Combobox(main_frame, textvariable=self.selected_model, width=50, font=("Arial", 10))
self.model_combo['values'] = ["Chargement des modèles..."]
self.model_combo.set("gpt-oss:120b-cloud")
self.model_combo.pack(anchor="w", pady=(5, 15))
# 3. Édition du Prompt
tk.Label(main_frame, text="Prompt pour la synthèse médicale :", bg="#f8f9fa", font=("Arial", 10, "bold")).pack(anchor="w")
self.prompt_text = tk.Text(main_frame, height=10, font=("Arial", 10), padx=5, pady=5)
self.prompt_text.pack(fill="x", pady=(5, 15))
default_prompt = (
"Tu es un expert médical assistant. Tu dois analyser la transcription d'une réunion médicale.\n"
"Ta mission est de produire une synthèse structurée incluant :\n"
"1. Objet de la réunion / Motif de consultation.\n"
"2. Éléments clés de la discussion (Symptômes, antécédents, examens évoqués).\n"
"3. Décisions prises ou Diagnostic provisoire.\n"
"4. Plan d'action (Traitements, examens complémentaires, prochain RDV).\n\n"
"Règle : Terminologie médicale précise et style synthétique."
)
self.prompt_text.insert("1.0", default_prompt)
# 4. Bouton de lancement
self.run_btn = tk.Button(main_frame, text="LANCER LE PIPELINE COMPLET",
bg="#28a745", fg="white", font=("Arial", 12, "bold"),
relief=tk.FLAT, pady=12, command=self.start_pipeline_thread)
self.run_btn.pack(fill="x", pady=10)
# 5. Zone de Logs
tk.Label(main_frame, text="Logs de traitement :", bg="#f8f9fa", font=("Arial", 10, "bold")).pack(anchor="w")
self.log_area = tk.Text(main_frame, height=12, bg="#1e1e1e", fg="#00ff00", font=("Consolas", 9), padx=10, pady=10)
self.log_area.pack(fill=tk.BOTH, expand=True, pady=5)
# Status bar
tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor="w", bg="#e9ecef").pack(side=tk.BOTTOM, fill=tk.X)
def browse_file(self):
filename = filedialog.askopenfilename(filetypes=[("Audio files", "*.wav *.mp3 *.m4a *.flac *.ogg")])
if filename:
self.audio_path.set(filename)
def load_ollama_models(self):
"""Tente de lister les modèles Ollama de manière robuste."""
try:
import ollama
print("[DEBUG] Connexion à Ollama...")
response = ollama.list()
# On essaie d'extraire les noms de différentes manières selon la version de l'API
models = []
if hasattr(response, 'models'):
models = response.models
elif isinstance(response, dict):
models = response.get('models', [])
model_names = []
for m in models:
if hasattr(m, 'model'): # Nouveau format
model_names.append(m.model)
elif isinstance(m, dict):
name = m.get('model') or m.get('name')
if name: model_names.append(name)
if model_names:
self.model_combo['values'] = model_names
if "gpt-oss:120b-cloud" in model_names:
self.selected_model.set("gpt-oss:120b-cloud")
elif "gpt-oss:latest" in model_names:
self.selected_model.set("gpt-oss:latest")
print(f"[DEBUG] {len(model_names)} modèles trouvés.")
else:
self.model_combo['values'] = ["gpt-oss:120b-cloud", "llama3.3:70b"]
except Exception as e:
print(f"[DEBUG] Erreur Ollama : {e}")
self.model_combo['values'] = ["gpt-oss:120b-cloud", "llama3.3:70b"]
def log(self, message):
self.log_area.insert(tk.END, message + "\n")
self.log_area.see(tk.END)
self.root.update_idletasks()
def start_pipeline_thread(self):
if not self.audio_path.get():
messagebox.showwarning("Attention", "Veuillez d'abord sélectionner un fichier audio.")
return
self.run_btn.config(state="disabled", bg="#6c757d")
self.log_area.delete("1.0", tk.END)
self.status_var.set("Traitement en cours... (Transcription + Diarisation)")
threading.Thread(target=self.run_pipeline, daemon=True).start()
def run_pipeline(self):
audio_file = self.audio_path.get()
model_name = self.selected_model.get()
custom_prompt = self.prompt_text.get("1.0", tk.END).strip()
try:
# Étape 1 : Diarisation et Transcription
self.log("--- ÉTAPE 1 : DIARISATION & TRANSCRIPTION WHISPER LARGE-V3 ---")
self.log(f"Fichier cible : {os.path.basename(audio_file)}")
script_dir = os.path.dirname(os.path.abspath(__file__))
env = os.environ.copy()
env["HF_TOKEN"] = HF_TOKEN
p = subprocess.Popen(
[sys.executable, os.path.join(script_dir, "medical_diarizer.py"), audio_file],
env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in p.stdout:
self.log(line.strip())
p.wait()
transcript_file = audio_file.rsplit('.', 1)[0] + "_diarized.txt"
if not os.path.exists(transcript_file):
raise Exception("Échec de la transcription : le fichier texte n'a pas été généré.")
# Étape 2 : Synthèse IA
self.log(f"\n--- ÉTAPE 2 : GÉNÉRATION SYNTHÈSE AVEC {model_name} ---")
self.status_var.set(f"Analyse par l'IA ({model_name})...")
with open(transcript_file, "r", encoding="utf-8") as f:
content = f.read()
import ollama
response = ollama.chat(
model=model_name,
messages=[
{"role": "system", "content": custom_prompt},
{"role": "user", "content": f"Voici le transcript à analyser :\n\n{content}"}
]
)
summary = response['message']['content']
summary_file = audio_file.rsplit('.', 1)[0] + "_summary.md"
with open(summary_file, "w", encoding="utf-8") as f:
f.write(summary)
self.log("\n" + "="*50)
self.log("PIPELINE MÉDICAL TERMINÉ !")
self.log(f"Transcript structuré : {os.path.basename(transcript_file)}")
self.log(f"Synthèse sauvegardée : {os.path.basename(summary_file)}")
self.log("="*50)
self.status_var.set("Terminé avec succès.")
messagebox.showinfo("Succès", "Traitement terminé avec succès !")
except Exception as e:
self.log(f"\n[ERREUR] {str(e)}")
self.status_var.set("Erreur durant le traitement.")
messagebox.showerror("Erreur de Pipeline", str(e))
finally:
self.run_btn.config(state="normal", bg="#28a745")
if __name__ == "__main__":
root = tk.Tk()
app = MedicalScribeGUI(root)
root.mainloop()