ramedde commited on
Commit
bf8de3d
·
verified ·
1 Parent(s): f431940

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -43
app.py CHANGED
@@ -7,13 +7,13 @@ import requests
7
  import json
8
  import gradio as gr
9
  from gtts import gTTS
10
-
11
  # ── llama-server connection ──────────────────────────────────────────────────
12
  LLAMA_URL = os.getenv("LLAMA_SERVER_URL", "http://127.0.0.1:8080")
13
  CHAT_ENDPOINT = f"{LLAMA_URL}/v1/chat/completions"
14
  _server_ok = False
15
  _server_lock = threading.Lock()
16
-
17
  def _check_server():
18
  """Background thread: poll until llama-server is ready."""
19
  global _server_ok
@@ -29,20 +29,20 @@ def _check_server():
29
  pass
30
  time.sleep(2)
31
  print("[law-app] llama-server not reachable – keyword fallback active")
32
-
33
  threading.Thread(target=_check_server, daemon=True).start()
34
-
35
  # ── Leçons ───────────────────────────────────────────────────────────────────
36
  with open("lessons.json") as f:
37
  LESSONS = json.load(f)
38
-
39
  LISTEN_SECONDS = 10 # 900 en production (15 min)
40
  PASS_SCORE = 6
41
-
42
  # ── Cache audio par leçon ─────────────────────────────────────────────────────
43
  _audio_cache = {}
44
  _audio_lock = threading.Lock()
45
-
46
  def get_lecture_audio(lesson_idx):
47
  if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
48
  return _audio_cache[lesson_idx]
@@ -59,15 +59,15 @@ def get_lecture_audio(lesson_idx):
59
  except Exception as e:
60
  print(f"[law-app] gTTS erreur : {e}")
61
  return None
62
-
63
  # ── État ──────────────────────────────────────────────────────────────────────
64
  def fresh():
65
  return json.dumps({"phase": "idle", "listen_start": None,
66
  "question_idx": None, "lesson_idx": 0})
67
-
68
  def S(s): return json.loads(s)
69
  def D(d): return json.dumps(d)
70
-
71
  # ── Évaluation ────────────────────────────────────────────────────────────────
72
  _SYSTEM_PROMPT = (
73
  "Tu es un professeur de droit français strict et bienveillant. "
@@ -90,18 +90,18 @@ _SYSTEM_PROMPT = (
90
  "Éléments attendus : L'OPJ (Officier) a le pouvoir de prendre des mesures coercitives comme le placement en garde à vue, l'APJ (Agent) le seconde.\n"
91
  '{"score": 1, "feedback": "C\'est incorrect. L\'APJ et l\'OPJ relèvent tous deux de la police judiciaire. La différence majeure réside dans les pouvoirs coercitifs (comme décider d\'une garde à vue) réservés à l\'OPJ, que l\'APJ seconde."}'
92
  )
93
-
94
  def _keyword_fallback(answer, hint, reason=""):
95
  kw = [w.strip().lower() for w in hint.split(",")]
96
  hits = sum(1 for k in kw if k in answer.lower())
97
  score = min(10, round((hits / max(len(kw), 1)) * 10))
98
  label = f"(Sans IA – {reason} – " if reason else "(Sans IA – "
99
  return score, f"{label}mots-clés : {hits}/{len(kw)})"
100
-
101
  def evaluate_answer(question, answer, hint):
102
  if not _server_ok:
103
  return _keyword_fallback(answer, hint, reason="serveur indisponible")
104
-
105
  user_msg = (
106
  f"Question : {question}\n"
107
  f"Réponse de l'étudiant : {answer}\n"
@@ -110,7 +110,7 @@ def evaluate_answer(question, answer, hint):
110
  "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n"
111
  '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}'
112
  )
113
-
114
  payload = {
115
  "model": "local",
116
  "messages": [
@@ -121,7 +121,7 @@ def evaluate_answer(question, answer, hint):
121
  "temperature": 0.1,
122
  "stop": ["\n\n", "###"],
123
  }
124
-
125
  try:
126
  resp = requests.post(CHAT_ENDPOINT, json=payload, timeout=60)
127
  resp.raise_for_status()
@@ -136,7 +136,7 @@ def evaluate_answer(question, answer, hint):
136
  except Exception as e:
137
  print(f"[law-app] Erreur éval : {e}")
138
  return _keyword_fallback(answer, hint, reason=str(e))
139
-
140
  # ── Handlers ──────────────────────────────────────────────────────────────────
141
  def start_listening(ss):
142
  state = S(ss)
@@ -158,7 +158,7 @@ def start_listening(ss):
158
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
159
  D(state),
160
  )
161
-
162
  def check_timer(ss):
163
  state = S(ss)
164
  if state["phase"] != "listen" or not state["listen_start"]:
@@ -176,7 +176,7 @@ def check_timer(ss):
176
  f"🎧 Patientez encore **{m} min {s:02d} s**.",
177
  ss,
178
  )
179
-
180
  def open_quiz(ss):
181
  state = S(ss)
182
  elapsed = time.time() - (state["listen_start"] or 0)
@@ -185,22 +185,29 @@ def open_quiz(ss):
185
  return (
186
  gr.update(visible=True), gr.update(visible=False),
187
  gr.update(visible=False), gr.update(visible=False),
188
- f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
 
 
 
189
  )
190
  lesson_idx = state.get("lesson_idx", 0)
191
  idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
192
  state.update({"phase": "quiz", "question_idx": idx})
 
193
  return (
194
  gr.update(visible=False), gr.update(visible=True),
195
  gr.update(visible=False), gr.update(visible=False),
196
- "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state),
 
 
 
197
  )
198
-
199
  # ── Submit avec désactivation immédiate du bouton ─────────────────────────────
200
  def submit_answer(answer, ss):
201
  state = S(ss)
202
  lesson_idx = state.get("lesson_idx", 0)
203
-
204
  if state["phase"] != "quiz":
205
  yield (
206
  gr.update(visible=True), gr.update(visible=False),
@@ -211,7 +218,7 @@ def submit_answer(answer, ss):
211
  ss,
212
  )
213
  return
214
-
215
  # Étape 1 : désactiver immédiatement bouton + textbox
216
  yield (
217
  gr.update(visible=False), gr.update(visible=True),
@@ -221,13 +228,13 @@ def submit_answer(answer, ss):
221
  gr.update(interactive=False),
222
  ss,
223
  )
224
-
225
  # Étape 2 : évaluation IA
226
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
227
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
228
  total = len(LESSONS)
229
  is_last = (lesson_idx >= total - 1)
230
-
231
  if score >= PASS_SCORE:
232
  state["phase"] = "pass"
233
  result = (
@@ -244,7 +251,7 @@ def submit_answer(answer, ss):
244
  "Veuillez **réécouter la leçon** avant de retenter."
245
  )
246
  next_label = "🔄 Réécouter la leçon"
247
-
248
  # Étape 3 : afficher résultat, réactiver bouton, mettre à jour next_btn
249
  yield (
250
  gr.update(visible=False), gr.update(visible=False),
@@ -254,15 +261,15 @@ def submit_answer(answer, ss):
254
  gr.update(value="", interactive=True),
255
  D(state),
256
  )
257
-
258
  def handle_next(ss):
259
  state = S(ss)
260
  lesson_idx = state.get("lesson_idx", 0)
261
  total = len(LESSONS)
262
-
263
  if state["phase"] == "fail":
264
  return start_listening(D(state))
265
-
266
  if lesson_idx >= total - 1:
267
  state["phase"] = "done"
268
  cert = (
@@ -290,22 +297,22 @@ def handle_next(ss):
290
  state["lesson_idx"] = lesson_idx + 1
291
  state["listen_start"] = None
292
  return start_listening(D(state))
293
-
294
  def restart_from_cert(ss):
295
  return start_listening(fresh())
296
-
297
  # ── UI ────────────────────────────────────────────────────────────────────────
298
  CSS = "footer{display:none!important;} #title{text-align:center;}"
299
-
300
  with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
301
  session = gr.State(fresh())
302
-
303
  gr.Markdown(
304
  "# ⚖️ Cours de Droit — Formation Juridique\n"
305
  "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon",
306
  elem_id="title",
307
  )
308
-
309
  with gr.Column(visible=True) as listen_col:
310
  gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon")
311
  listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
@@ -315,43 +322,44 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
315
  start_btn = gr.Button("▶️ Démarrer l'écoute", variant="primary")
316
  check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
317
  quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
318
-
319
  with gr.Column(visible=False) as quiz_col:
320
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
321
  question_md = gr.Markdown("")
322
  answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
323
  submit_btn = gr.Button("📤 Soumettre", variant="primary")
324
-
325
  with gr.Column(visible=False) as result_col:
326
  gr.Markdown("## 📊 Résultat")
327
  result_md = gr.Markdown("")
328
  next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
329
-
330
  with gr.Column(visible=False) as cert_col:
331
  cert_md = gr.Markdown("")
332
  restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
333
-
334
  # ── Câblage ──────────────────────────────────────────────────────��────────
335
  LISTEN_OUT = [audio_out, listen_msg,
336
  listen_col, quiz_col, result_col, cert_col,
337
  quiz_btn, session]
338
-
339
  start_btn.click(start_listening, [session], LISTEN_OUT)
340
  check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
341
-
342
  QUIZ_OUT = [listen_col, quiz_col, result_col, cert_col,
343
  timer_info, question_md, answer_box, session]
344
  quiz_btn.click(open_quiz, [session], QUIZ_OUT)
345
-
346
  # submit_answer est un générateur → outputs inclut submit_btn et answer_box
347
  RES_OUT = [listen_col, quiz_col, result_col, cert_col,
348
  result_md, submit_btn, answer_box, session]
349
  submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
350
  answer_box.submit(submit_answer, [answer_box, session], RES_OUT)
351
-
352
  next_btn.click(handle_next, [session], LISTEN_OUT)
353
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
354
-
355
  if __name__ == "__main__":
356
  demo.queue()
357
  demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
 
 
7
  import json
8
  import gradio as gr
9
  from gtts import gTTS
10
+
11
  # ── llama-server connection ──────────────────────────────────────────────────
12
  LLAMA_URL = os.getenv("LLAMA_SERVER_URL", "http://127.0.0.1:8080")
13
  CHAT_ENDPOINT = f"{LLAMA_URL}/v1/chat/completions"
14
  _server_ok = False
15
  _server_lock = threading.Lock()
16
+
17
  def _check_server():
18
  """Background thread: poll until llama-server is ready."""
19
  global _server_ok
 
29
  pass
30
  time.sleep(2)
31
  print("[law-app] llama-server not reachable – keyword fallback active")
32
+
33
  threading.Thread(target=_check_server, daemon=True).start()
34
+
35
  # ── Leçons ───────────────────────────────────────────────────────────────────
36
  with open("lessons.json") as f:
37
  LESSONS = json.load(f)
38
+
39
  LISTEN_SECONDS = 10 # 900 en production (15 min)
40
  PASS_SCORE = 6
41
+
42
  # ── Cache audio par leçon ─────────────────────────────────────────────────────
43
  _audio_cache = {}
44
  _audio_lock = threading.Lock()
45
+
46
  def get_lecture_audio(lesson_idx):
47
  if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
48
  return _audio_cache[lesson_idx]
 
59
  except Exception as e:
60
  print(f"[law-app] gTTS erreur : {e}")
61
  return None
62
+
63
  # ── État ──────────────────────────────────────────────────────────────────────
64
  def fresh():
65
  return json.dumps({"phase": "idle", "listen_start": None,
66
  "question_idx": None, "lesson_idx": 0})
67
+
68
  def S(s): return json.loads(s)
69
  def D(d): return json.dumps(d)
70
+
71
  # ── Évaluation ────────────────────────────────────────────────────────────────
72
  _SYSTEM_PROMPT = (
73
  "Tu es un professeur de droit français strict et bienveillant. "
 
90
  "Éléments attendus : L'OPJ (Officier) a le pouvoir de prendre des mesures coercitives comme le placement en garde à vue, l'APJ (Agent) le seconde.\n"
91
  '{"score": 1, "feedback": "C\'est incorrect. L\'APJ et l\'OPJ relèvent tous deux de la police judiciaire. La différence majeure réside dans les pouvoirs coercitifs (comme décider d\'une garde à vue) réservés à l\'OPJ, que l\'APJ seconde."}'
92
  )
93
+
94
  def _keyword_fallback(answer, hint, reason=""):
95
  kw = [w.strip().lower() for w in hint.split(",")]
96
  hits = sum(1 for k in kw if k in answer.lower())
97
  score = min(10, round((hits / max(len(kw), 1)) * 10))
98
  label = f"(Sans IA – {reason} – " if reason else "(Sans IA – "
99
  return score, f"{label}mots-clés : {hits}/{len(kw)})"
100
+
101
  def evaluate_answer(question, answer, hint):
102
  if not _server_ok:
103
  return _keyword_fallback(answer, hint, reason="serveur indisponible")
104
+
105
  user_msg = (
106
  f"Question : {question}\n"
107
  f"Réponse de l'étudiant : {answer}\n"
 
110
  "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n"
111
  '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}'
112
  )
113
+
114
  payload = {
115
  "model": "local",
116
  "messages": [
 
121
  "temperature": 0.1,
122
  "stop": ["\n\n", "###"],
123
  }
124
+
125
  try:
126
  resp = requests.post(CHAT_ENDPOINT, json=payload, timeout=60)
127
  resp.raise_for_status()
 
136
  except Exception as e:
137
  print(f"[law-app] Erreur éval : {e}")
138
  return _keyword_fallback(answer, hint, reason=str(e))
139
+
140
  # ── Handlers ──────────────────────────────────────────────────────────────────
141
  def start_listening(ss):
142
  state = S(ss)
 
158
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
159
  D(state),
160
  )
161
+
162
  def check_timer(ss):
163
  state = S(ss)
164
  if state["phase"] != "listen" or not state["listen_start"]:
 
176
  f"🎧 Patientez encore **{m} min {s:02d} s**.",
177
  ss,
178
  )
179
+
180
  def open_quiz(ss):
181
  state = S(ss)
182
  elapsed = time.time() - (state["listen_start"] or 0)
 
185
  return (
186
  gr.update(visible=True), gr.update(visible=False),
187
  gr.update(visible=False), gr.update(visible=False),
188
+ gr.update(value=f"⚠️ Patientez encore **{m} min {s:02d} s**."),
189
+ gr.update(value=""),
190
+ gr.update(value=""),
191
+ ss,
192
  )
193
  lesson_idx = state.get("lesson_idx", 0)
194
  idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
195
  state.update({"phase": "quiz", "question_idx": idx})
196
+ question_text = LESSONS[lesson_idx]["questions"][idx]["q"]
197
  return (
198
  gr.update(visible=False), gr.update(visible=True),
199
  gr.update(visible=False), gr.update(visible=False),
200
+ gr.update(value=""),
201
+ gr.update(value=question_text),
202
+ gr.update(value=""),
203
+ D(state),
204
  )
205
+
206
  # ── Submit avec désactivation immédiate du bouton ─────────────────────────────
207
  def submit_answer(answer, ss):
208
  state = S(ss)
209
  lesson_idx = state.get("lesson_idx", 0)
210
+
211
  if state["phase"] != "quiz":
212
  yield (
213
  gr.update(visible=True), gr.update(visible=False),
 
218
  ss,
219
  )
220
  return
221
+
222
  # Étape 1 : désactiver immédiatement bouton + textbox
223
  yield (
224
  gr.update(visible=False), gr.update(visible=True),
 
228
  gr.update(interactive=False),
229
  ss,
230
  )
231
+
232
  # Étape 2 : évaluation IA
233
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
234
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
235
  total = len(LESSONS)
236
  is_last = (lesson_idx >= total - 1)
237
+
238
  if score >= PASS_SCORE:
239
  state["phase"] = "pass"
240
  result = (
 
251
  "Veuillez **réécouter la leçon** avant de retenter."
252
  )
253
  next_label = "🔄 Réécouter la leçon"
254
+
255
  # Étape 3 : afficher résultat, réactiver bouton, mettre à jour next_btn
256
  yield (
257
  gr.update(visible=False), gr.update(visible=False),
 
261
  gr.update(value="", interactive=True),
262
  D(state),
263
  )
264
+
265
  def handle_next(ss):
266
  state = S(ss)
267
  lesson_idx = state.get("lesson_idx", 0)
268
  total = len(LESSONS)
269
+
270
  if state["phase"] == "fail":
271
  return start_listening(D(state))
272
+
273
  if lesson_idx >= total - 1:
274
  state["phase"] = "done"
275
  cert = (
 
297
  state["lesson_idx"] = lesson_idx + 1
298
  state["listen_start"] = None
299
  return start_listening(D(state))
300
+
301
  def restart_from_cert(ss):
302
  return start_listening(fresh())
303
+
304
  # ── UI ────────────────────────────────────────────────────────────────────────
305
  CSS = "footer{display:none!important;} #title{text-align:center;}"
306
+
307
  with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
308
  session = gr.State(fresh())
309
+
310
  gr.Markdown(
311
  "# ⚖️ Cours de Droit — Formation Juridique\n"
312
  "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon",
313
  elem_id="title",
314
  )
315
+
316
  with gr.Column(visible=True) as listen_col:
317
  gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon")
318
  listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
 
322
  start_btn = gr.Button("▶️ Démarrer l'écoute", variant="primary")
323
  check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
324
  quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
325
+
326
  with gr.Column(visible=False) as quiz_col:
327
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
328
  question_md = gr.Markdown("")
329
  answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
330
  submit_btn = gr.Button("📤 Soumettre", variant="primary")
331
+
332
  with gr.Column(visible=False) as result_col:
333
  gr.Markdown("## 📊 Résultat")
334
  result_md = gr.Markdown("")
335
  next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
336
+
337
  with gr.Column(visible=False) as cert_col:
338
  cert_md = gr.Markdown("")
339
  restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
340
+
341
  # ── Câblage ──────────────────────────────────────────────────────��────────
342
  LISTEN_OUT = [audio_out, listen_msg,
343
  listen_col, quiz_col, result_col, cert_col,
344
  quiz_btn, session]
345
+
346
  start_btn.click(start_listening, [session], LISTEN_OUT)
347
  check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
348
+
349
  QUIZ_OUT = [listen_col, quiz_col, result_col, cert_col,
350
  timer_info, question_md, answer_box, session]
351
  quiz_btn.click(open_quiz, [session], QUIZ_OUT)
352
+
353
  # submit_answer est un générateur → outputs inclut submit_btn et answer_box
354
  RES_OUT = [listen_col, quiz_col, result_col, cert_col,
355
  result_md, submit_btn, answer_box, session]
356
  submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
357
  answer_box.submit(submit_answer, [answer_box, session], RES_OUT)
358
+
359
  next_btn.click(handle_next, [session], LISTEN_OUT)
360
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
361
+
362
  if __name__ == "__main__":
363
  demo.queue()
364
  demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
365
+