245 lines
7.0 KiB
Python
245 lines
7.0 KiB
Python
import base64
|
|
import json
|
|
import os
|
|
import smtplib
|
|
import tempfile
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
from groq import Groq
|
|
|
|
load_dotenv()
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
STATIC_DIR = ROOT / "static"
|
|
CONTACTS_FILE = ROOT / "contacts.json"
|
|
|
|
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
|
|
GMAIL_EMAIL = os.getenv("GMAIL_EMAIL", "")
|
|
GMAIL_PASSWORD = os.getenv("GMAIL_PASSWORD", "")
|
|
|
|
CHAT_MODEL = "llama-3.3-70b-versatile"
|
|
STT_MODEL = "whisper-large-v3-turbo"
|
|
TTS_MODEL = "canopylabs/orpheus-v1-english"
|
|
TTS_VOICE = "troy"
|
|
TTS_PROVIDER = os.getenv("TTS_PROVIDER", "browser").lower()
|
|
|
|
app = Flask(__name__, static_folder=str(STATIC_DIR), static_url_path="")
|
|
app.secret_key = os.getenv("APP_SECRET", "dev-secret-change-me")
|
|
|
|
client = Groq(api_key=GROQ_API_KEY)
|
|
histories = {}
|
|
|
|
SYSTEM_PROMPT = """Tu es l'assistant personnel vocal de Laurent.
|
|
Tu peux discuter normalement en francais et aider a envoyer des emails via Gmail.
|
|
|
|
Quand l'utilisateur veut envoyer un email, collecte les informations manquantes :
|
|
destinataire, sujet, contenu.
|
|
|
|
Quand tu as toutes les informations pour envoyer un email, reponds uniquement avec ce JSON :
|
|
{"action": "envoyer_email", "destinataire": "...", "sujet": "...", "corps": "..."}
|
|
|
|
Pour tout le reste, reponds en francais naturel, avec des phrases courtes.
|
|
N'utilise pas d'anglais sauf si l'utilisateur le demande explicitement.
|
|
En mode voiture, sois bref : une ou deux phrases maximum."""
|
|
|
|
|
|
def charger_contacts():
|
|
try:
|
|
return json.loads(CONTACTS_FILE.read_text(encoding="utf-8"))
|
|
except FileNotFoundError:
|
|
return {"laurent": "barontil@hotmail.com"}
|
|
|
|
|
|
def construire_system_prompt():
|
|
contacts = charger_contacts()
|
|
lignes = [f"- {nom.title()} : {email}" for nom, email in contacts.items()]
|
|
return SYSTEM_PROMPT + "\n\nContacts connus :\n" + "\n".join(lignes)
|
|
|
|
|
|
def resoudre_contact(destinataire):
|
|
contacts = charger_contacts()
|
|
valeur = destinataire.strip()
|
|
return contacts.get(valeur.lower(), valeur)
|
|
|
|
|
|
def extraire_action_email(reponse):
|
|
decodeur = json.JSONDecoder()
|
|
for index, caractere in enumerate(reponse):
|
|
if caractere != "{":
|
|
continue
|
|
try:
|
|
data, _ = decodeur.raw_decode(reponse[index:])
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(data, dict) and data.get("action") == "envoyer_email":
|
|
return data
|
|
return None
|
|
|
|
|
|
def envoyer_email(destinataire, sujet, corps):
|
|
destinataire = resoudre_contact(destinataire)
|
|
msg = MIMEMultipart()
|
|
msg["From"] = GMAIL_EMAIL
|
|
msg["To"] = destinataire
|
|
msg["Subject"] = sujet
|
|
msg.attach(MIMEText(corps, "plain", "utf-8"))
|
|
|
|
with smtplib.SMTP("smtp.gmail.com", 587) as serveur:
|
|
serveur.starttls()
|
|
serveur.login(GMAIL_EMAIL, GMAIL_PASSWORD)
|
|
serveur.sendmail(GMAIL_EMAIL, destinataire, msg.as_string())
|
|
|
|
return destinataire
|
|
|
|
|
|
def demander_au_llm(session_id, texte):
|
|
historique = histories.setdefault(session_id, [])
|
|
historique.append({"role": "user", "content": texte})
|
|
del historique[:-20]
|
|
|
|
reponse = client.chat.completions.create(
|
|
model=CHAT_MODEL,
|
|
messages=[{"role": "system", "content": construire_system_prompt()}] + historique,
|
|
temperature=0.45,
|
|
max_tokens=900,
|
|
)
|
|
contenu = reponse.choices[0].message.content
|
|
historique.append({"role": "assistant", "content": contenu})
|
|
return contenu
|
|
|
|
|
|
def transcrire_audio(fichier):
|
|
suffix = Path(fichier.filename or "voice.webm").suffix or ".webm"
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
|
fichier.save(tmp)
|
|
chemin = tmp.name
|
|
|
|
try:
|
|
with open(chemin, "rb") as audio:
|
|
transcription = client.audio.transcriptions.create(
|
|
model=STT_MODEL,
|
|
file=audio,
|
|
language="fr",
|
|
response_format="text",
|
|
)
|
|
return transcription if isinstance(transcription, str) else transcription.text
|
|
finally:
|
|
try:
|
|
os.remove(chemin)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def generer_audio(texte):
|
|
texte = texte.strip()
|
|
if len(texte) > 900:
|
|
texte = texte[:897] + "..."
|
|
|
|
reponse_audio = client.audio.speech.create(
|
|
model=TTS_MODEL,
|
|
voice=TTS_VOICE,
|
|
input=texte,
|
|
response_format="wav",
|
|
speed=1.0,
|
|
)
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
|
|
chemin = tmp.name
|
|
|
|
try:
|
|
reponse_audio.write_to_file(chemin)
|
|
audio = Path(chemin).read_bytes()
|
|
return "data:audio/wav;base64," + base64.b64encode(audio).decode("ascii")
|
|
finally:
|
|
try:
|
|
os.remove(chemin)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def traiter_message(session_id, texte, avec_audio=False):
|
|
reponse = demander_au_llm(session_id, texte)
|
|
action = extraire_action_email(reponse)
|
|
email_sent = False
|
|
|
|
if action:
|
|
destinataire = envoyer_email(action["destinataire"], action["sujet"], action["corps"])
|
|
reponse = f"Email envoye a {destinataire}."
|
|
email_sent = True
|
|
|
|
audio_url = None
|
|
if avec_audio and TTS_PROVIDER == "groq":
|
|
audio_url = generer_audio(reponse)
|
|
|
|
return {
|
|
"reply": reponse,
|
|
"audioUrl": audio_url,
|
|
"emailSent": email_sent,
|
|
}
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return send_from_directory(STATIC_DIR, "index.html")
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@app.post("/api/chat")
|
|
def chat():
|
|
data = request.get_json(force=True)
|
|
texte = (data.get("text") or "").strip()
|
|
session_id = data.get("sessionId") or "default"
|
|
avec_audio = bool(data.get("withAudio"))
|
|
|
|
if not texte:
|
|
return jsonify({"error": "Message vide"}), 400
|
|
|
|
try:
|
|
return jsonify(traiter_message(session_id, texte, avec_audio))
|
|
except Exception as e:
|
|
print(f"Erreur /api/chat : {e}", flush=True)
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.post("/api/voice")
|
|
def voice():
|
|
fichier = request.files.get("audio")
|
|
session_id = request.form.get("sessionId") or "default"
|
|
avec_audio = request.form.get("withAudio", "false").lower() == "true"
|
|
|
|
if not fichier:
|
|
return jsonify({"error": "Audio manquant"}), 400
|
|
|
|
try:
|
|
texte = transcrire_audio(fichier).strip()
|
|
resultat = traiter_message(session_id, texte, avec_audio=avec_audio)
|
|
resultat["transcript"] = texte
|
|
return jsonify(resultat)
|
|
except Exception as e:
|
|
print(f"Erreur /api/voice : {e}", flush=True)
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.post("/api/reset")
|
|
def reset():
|
|
data = request.get_json(silent=True) or {}
|
|
session_id = data.get("sessionId") or "default"
|
|
histories.pop(session_id, None)
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(
|
|
host="0.0.0.0",
|
|
port=int(os.getenv("PORT", "8080")),
|
|
debug=os.getenv("FLASK_DEBUG", "0") == "1",
|
|
)
|