197 lines
5.9 KiB
Python
197 lines
5.9 KiB
Python
import asyncio
|
|
import sys
|
|
from . import llm, tts, audio, config
|
|
|
|
|
|
HELP_TEXT = """
|
|
Commandes disponibles :
|
|
exit / quit Quitter l'assistant
|
|
reset Effacer l'historique de conversation
|
|
voice <id> Changer la voix Voxtral (voice_id)
|
|
voice clear Revenir à la voix par défaut
|
|
mode texte Passer en mode saisie texte (défaut)
|
|
mode vocal Passer en mode entrée microphone
|
|
profiles Lister les profils de personnalité disponibles
|
|
profile <slug> Charger un profil (ex: profile traveller_scout)
|
|
mcp Lister les serveurs MCP connectés
|
|
mcp tools Lister tous les outils MCP disponibles
|
|
help Afficher ce message
|
|
|
|
Mode vocal : appuyez sur Entrée (sans rien écrire) pour commencer à parler,
|
|
puis Entrée à nouveau pour envoyer.
|
|
"""
|
|
|
|
|
|
def _set_voice(parts: list[str]) -> None:
|
|
if len(parts) < 2:
|
|
print("Usage : voice <id> ou voice clear")
|
|
return
|
|
if parts[1] == "clear":
|
|
config.VOICE_ID = None
|
|
print("Voix réinitialisée (défaut).")
|
|
else:
|
|
config.VOICE_ID = parts[1]
|
|
print(f"Voix définie sur : {config.VOICE_ID}")
|
|
|
|
|
|
def _process_message(user_input: str) -> None:
|
|
"""Envoie un message au LLM et lit la réponse à voix haute."""
|
|
print(f"Arioch > ", end="", flush=True)
|
|
try:
|
|
reply = llm.chat(user_input)
|
|
except Exception as e:
|
|
print(f"\n[Erreur LLM] {e}")
|
|
return
|
|
|
|
print(reply)
|
|
|
|
try:
|
|
audio_bytes = tts.text_to_speech(reply)
|
|
audio.play_audio(audio_bytes)
|
|
except Exception as e:
|
|
print(f"[Erreur TTS/Audio] {e}")
|
|
|
|
|
|
def _handle_command(user_input: str) -> bool:
|
|
"""Gère les commandes spéciales. Retourne True si c'était une commande."""
|
|
from .profile import list_profiles, apply_profile
|
|
|
|
lower = user_input.lower()
|
|
parts = user_input.split()
|
|
|
|
if lower in ("exit", "quit"):
|
|
print("Au revoir !")
|
|
sys.exit(0)
|
|
elif lower == "reset":
|
|
llm.reset_history()
|
|
print("Historique effacé.\n")
|
|
return True
|
|
elif lower == "help":
|
|
print(HELP_TEXT)
|
|
return True
|
|
elif lower.startswith("voice"):
|
|
_set_voice(parts)
|
|
return True
|
|
elif lower in ("mode texte", "mode text"):
|
|
return True # signal au caller
|
|
elif lower in ("mode vocal", "mode voix", "mode voice"):
|
|
return True # signal au caller
|
|
elif lower == "profiles":
|
|
_list_profiles(list_profiles())
|
|
return True
|
|
elif lower.startswith("mcp"):
|
|
_handle_mcp(parts)
|
|
return True
|
|
elif lower.startswith("profile ") and len(parts) >= 2:
|
|
_load_profile(parts[1], apply_profile)
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _handle_mcp(parts: list[str]) -> None:
|
|
from . import mcp_client
|
|
manager = mcp_client.get_manager()
|
|
servers = manager.summary()
|
|
|
|
if len(parts) >= 2 and parts[1] == "tools":
|
|
tools = manager.get_mistral_tools()
|
|
if not tools:
|
|
print("Aucun outil MCP disponible.")
|
|
return
|
|
print(f"\n{len(tools)} outil(s) MCP disponible(s) :")
|
|
for t in tools:
|
|
fn = t["function"]
|
|
desc = fn.get("description", "")
|
|
print(f" {fn['name']:<45} {desc[:60]}")
|
|
print()
|
|
return
|
|
|
|
if not servers:
|
|
print("Aucun serveur MCP connecté. Configurez 'mcp_servers' dans un profil YAML.\n")
|
|
return
|
|
|
|
print("\nServeurs MCP connectés :")
|
|
for name, count in servers:
|
|
print(f" {name:<30} {count} outil(s)")
|
|
total = sum(c for _, c in servers)
|
|
print(f"\nTotal : {total} outil(s). Tapez 'mcp tools' pour les lister.\n")
|
|
|
|
|
|
|
|
if not profiles:
|
|
print("Aucun profil disponible dans profiles/")
|
|
return
|
|
print("\nProfils disponibles :")
|
|
for slug, name, desc in profiles:
|
|
print(f" {slug:<25} {name}" + (f" — {desc}" if desc else ""))
|
|
print("\nUsage : profile <slug>\n")
|
|
|
|
|
|
def _load_profile(slug: str, apply_fn) -> None:
|
|
try:
|
|
profile = apply_fn(slug)
|
|
print(f"✅ Profil chargé : {profile.name}")
|
|
if profile.description:
|
|
print(f" {profile.description}")
|
|
print()
|
|
except FileNotFoundError as e:
|
|
print(f"[Profil] {e}")
|
|
|
|
|
|
def run() -> None:
|
|
print("🎙️ Arioch — Assistant vocal (Mistral Large + Voxtral)")
|
|
print("Commandes : 'profiles' pour voir les personnalités, 'mode vocal' pour parler, 'help' pour l'aide.\n")
|
|
|
|
vocal_mode = False
|
|
|
|
while True:
|
|
try:
|
|
if vocal_mode:
|
|
prompt = "🎤 [vocal] Entrée pour parler > "
|
|
else:
|
|
prompt = "Vous > "
|
|
|
|
user_input = input(prompt).strip()
|
|
except (EOFError, KeyboardInterrupt):
|
|
print("\nAu revoir !")
|
|
sys.exit(0)
|
|
|
|
if not user_input:
|
|
if vocal_mode:
|
|
# Entrée vide en mode vocal → lancer la capture micro
|
|
try:
|
|
from .stt import transcribe_from_mic
|
|
user_input = asyncio.run(transcribe_from_mic())
|
|
except Exception as e:
|
|
print(f"[Erreur STT] {e}")
|
|
continue
|
|
|
|
if not user_input:
|
|
print("(rien capturé)")
|
|
continue
|
|
|
|
print(f"Vous (transcrit) : {user_input}")
|
|
_process_message(user_input)
|
|
continue
|
|
|
|
lower = user_input.lower()
|
|
|
|
# Changement de mode
|
|
if lower in ("mode vocal", "mode voix", "mode voice"):
|
|
vocal_mode = True
|
|
print("Mode vocal activé. Appuyez sur Entrée (sans rien écrire) pour parler.\n")
|
|
continue
|
|
elif lower in ("mode texte", "mode text"):
|
|
vocal_mode = False
|
|
print("Mode texte activé.\n")
|
|
continue
|
|
|
|
# Autres commandes
|
|
if _handle_command(user_input):
|
|
continue
|
|
|
|
# Message normal (texte)
|
|
_process_message(user_input)
|
|
|