202 lines
8.8 KiB
Python
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()
|