import logging import re import os import tempfile import time log = logging.getLogger(__name__) from prompts import SYSTEM_PROMPT, PHASE_SWITCH_REMINDER from parse_feedback import parse_feedback, render_feedback_table, strip_markdown from stt_engine import transcribe, warmup as stt_warmup from llm_engine import chat as llm_chat, warmup as llm_warmup from tts_engine import synthesize TERMINATE_RE = re.compile( r"(fin\s+de\s+(la\s+)?séance|session\s+terminée|on\s+a\s+terminé|c'est\s+fini)", re.IGNORECASE, ) def make_initial_state(): messages = [{"role": "system", "content": SYSTEM_PROMPT}] return {"messages": messages, "phase": 1, "turn_count": 0} def _chat_val(state): return [m for m in state["messages"] if m.get("content") != PHASE_SWITCH_REMINDER] def _make_audio(audio_bytes): if not audio_bytes: return None f = tempfile.NamedTemporaryFile(suffix=".wav", delete=False, dir=tempfile.gettempdir()) f.write(audio_bytes) f.close() return os.path.basename(f.name) def _default_feedback(): """Return a blank feedback result block.""" return { "feedback_intro": "", "feedback_points_forts": [], "feedback_table": [], "feedback_vocabulaire": [], "feedback_priorite": [], "feedback_bilan": {}, "feedback_open": False, } def process_turn(audio_path, state): state = dict(state) result = { "chat": _chat_val(state), "audio_file": None, "state": state, **_default_feedback(), "status": "", } if not audio_path: return result result["status"] = "🎙 Transcription…" _t0 = time.monotonic() user_text = transcribe(audio_path) _t1 = time.monotonic() if not user_text or len(user_text.strip()) < 2: result["status"] = "⛔ Parlez plus fort ou plus longtemps." return result state["messages"].append({"role": "user", "content": user_text.strip()}) if TERMINATE_RE.search(user_text): return _end_session(state) result["status"] = "🧠 Réflexion…" response = llm_chat(state["messages"]) _t2 = time.monotonic() if not response: result["status"] = "⛔ Erreur du modèle. Réessayez." return result clean = strip_markdown(response) state["messages"].append({"role": "assistant", "content": clean}) result["status"] = "🔊 Synthèse vocale…" audio_bytes = synthesize(clean) _t3 = time.monotonic() result["audio_file"] = _make_audio(audio_bytes) log.info("LATENCY stt=%.1fs llm=%.1fs tts=%.1fs total=%.1fs", _t1 - _t0, _t2 - _t1, _t3 - _t2, _t3 - _t0) result["chat"] = _chat_val(state) result["status"] = "" return result def _end_session(state): state["messages"].append({"role": "user", "content": PHASE_SWITCH_REMINDER}) result = { "chat": _chat_val(state), "audio_file": None, "state": state, **_default_feedback(), "status": "📝 Génération du récapitulatif…", } response = llm_chat(state["messages"]) _t0_base = time.monotonic() _t0 = _t0_base if not response: result["status"] = "⛔ Erreur lors de la génération du bilan." return result clean = strip_markdown(response) state["phase"] = 2 fb = parse_feedback(clean) table = render_feedback_table(fb["erreurs"]) if fb["erreurs"] else [] intro = fb.get("intro") or clean state["messages"].append({"role": "assistant", "content": intro}) audio_bytes = synthesize(intro) _t1 = time.monotonic() result["chat"] = _chat_val(state) result["audio_file"] = _make_audio(audio_bytes) log.info("LATENCY llm=%.1fs tts=%.1fs total=%.1fs", _t0 - _t0_base, _t1 - _t0, _t1 - _t0_base) result["feedback_intro"] = intro result["feedback_points_forts"] = fb["points_forts"] result["feedback_table"] = table result["feedback_vocabulaire"] = fb["vocabulaire"] result["feedback_priorite"] = fb["priorite"] result["feedback_bilan"] = fb["bilan"] result["feedback_open"] = True result["status"] = "" return result def end_session_click(state): return _end_session(dict(state)) def reset_session(): stt_warmup() llm_warmup() return make_initial_state()