File size: 4,338 Bytes
f234692
7825141
 
 
f234692
 
 
7825141
 
 
eb93c12
 
7825141
 
864a4d0
 
 
 
7825141
 
 
 
 
 
 
 
 
 
 
 
 
 
6f92dc6
7825141
 
 
 
 
864a4d0
 
 
 
 
 
 
 
 
 
 
 
 
7825141
 
 
 
 
 
864a4d0
7825141
 
 
 
 
 
 
f234692
7825141
f234692
7825141
 
 
 
 
 
 
 
 
 
 
f234692
7825141
 
 
 
 
 
 
 
 
f234692
7825141
f234692
 
7825141
 
 
 
 
 
 
 
 
 
 
 
864a4d0
7825141
 
 
 
f234692
 
7825141
 
 
 
 
 
 
864a4d0
 
 
7825141
7da7aca
 
7825141
f234692
7825141
 
 
f234692
 
7825141
864a4d0
7825141
864a4d0
 
 
7825141
 
 
 
 
 
 
 
 
 
eb93c12
 
7825141
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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()