carlosduplar commited on
Commit
eb93c12
·
1 Parent(s): f6d088b

feat: LLM warmup endpoint + first-turn status message

Browse files

- Add /warmup endpoint to Modal LLM app to pre-load model on page load
- Add llm_engine.warmup() with rate limiting, called from reset_session()
- Show 'patient on their way' status on first audio turn in frontend
before processing begins, setting expectations for cold start delay

Files changed (4) hide show
  1. core.py +4 -3
  2. custom_index.html +7 -0
  3. llm_engine.py +34 -0
  4. modal_app.py +10 -9
core.py CHANGED
@@ -4,8 +4,8 @@ import tempfile
4
 
5
  from prompts import SYSTEM_PROMPT, PHASE_SWITCH_REMINDER
6
  from parse_feedback import parse_feedback, render_feedback_table, strip_markdown
7
- from stt_engine import transcribe, warmup
8
- from llm_engine import chat as llm_chat
9
  from tts_engine import synthesize
10
 
11
  TERMINATE_RE = re.compile(
@@ -131,5 +131,6 @@ def end_session_click(state):
131
 
132
 
133
  def reset_session():
134
- warmup()
 
135
  return make_initial_state()
 
4
 
5
  from prompts import SYSTEM_PROMPT, PHASE_SWITCH_REMINDER
6
  from parse_feedback import parse_feedback, render_feedback_table, strip_markdown
7
+ from stt_engine import transcribe, warmup as stt_warmup
8
+ from llm_engine import chat as llm_chat, warmup as llm_warmup
9
  from tts_engine import synthesize
10
 
11
  TERMINATE_RE = re.compile(
 
131
 
132
 
133
  def reset_session():
134
+ stt_warmup()
135
+ llm_warmup()
136
  return make_initial_state()
custom_index.html CHANGED
@@ -776,6 +776,13 @@ function stopRecording() {
776
  // Process audio turn
777
  async function processAudio(blob) {
778
  if (!client) return;
 
 
 
 
 
 
 
779
  isProcessing = true;
780
  micBtn.disabled = true;
781
  btnEnd.disabled = true;
 
776
  // Process audio turn
777
  async function processAudio(blob) {
778
  if (!client) return;
779
+
780
+ const isFirstTurn = !sessionState || sessionState.turn_count === 0;
781
+ if (isFirstTurn) {
782
+ setStatus("🚶 Le patient arrive au cabinet… veuillez patienter.");
783
+ await new Promise(r => setTimeout(r, 2000));
784
+ }
785
+
786
  isProcessing = true;
787
  micBtn.disabled = true;
788
  btnEnd.disabled = true;
llm_engine.py CHANGED
@@ -1,4 +1,7 @@
1
  import os
 
 
 
2
  import httpx
3
 
4
  MODAL_ENDPOINT = os.environ.get("MODAL_ENDPOINT_QWEN", "")
@@ -6,6 +9,37 @@ MODAL_AUTH_TOKEN = os.environ.get("MODAL_AUTH_TOKEN", "")
6
 
7
  TRUNCATION_LIMIT = 20_000
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def chat(messages: list[dict]) -> str | None:
11
  if not MODAL_ENDPOINT:
 
1
  import os
2
+ import threading
3
+ import time
4
+
5
  import httpx
6
 
7
  MODAL_ENDPOINT = os.environ.get("MODAL_ENDPOINT_QWEN", "")
 
9
 
10
  TRUNCATION_LIMIT = 20_000
11
 
12
+ _last_warmup = 0.0
13
+ _warmup_lock = threading.Lock()
14
+
15
+
16
+ def warmup():
17
+ """Send a lightweight request to Modal to trigger LLM container spin-up.
18
+
19
+ Rate-limited to once per 120s to avoid spamming on every page load.
20
+ """
21
+ global _last_warmup
22
+ if not MODAL_ENDPOINT:
23
+ return
24
+
25
+ now = time.monotonic()
26
+ with _warmup_lock:
27
+ if now - _last_warmup < 120:
28
+ return
29
+ _last_warmup = now
30
+
31
+ def _ping():
32
+ try:
33
+ resp = httpx.post(
34
+ MODAL_ENDPOINT + "/warmup",
35
+ json={"token": MODAL_AUTH_TOKEN},
36
+ timeout=120,
37
+ )
38
+ except Exception:
39
+ pass
40
+
41
+ threading.Thread(target=_ping, daemon=True).start()
42
+
43
 
44
  def chat(messages: list[dict]) -> str | None:
45
  if not MODAL_ENDPOINT:
modal_app.py CHANGED
@@ -50,15 +50,6 @@ async def _load_llm():
50
  n_threads=4,
51
  verbose=False,
52
  )
53
- try:
54
- draft_path = hf_hub_download(
55
- repo_id="unsloth/gemma-4-26B-A4B-it-GGUF",
56
- filename="mtp-gemma-4-26B-A4B-it.gguf",
57
- cache_dir=CACHE_DIR,
58
- )
59
- kwargs["draft_model"] = draft_path
60
- except Exception:
61
- pass
62
  _llm_model = Llama(**kwargs)
63
  await llm_vol.commit.aio()
64
 
@@ -122,6 +113,16 @@ def llm_web():
122
  },
123
  }
124
 
 
 
 
 
 
 
 
 
 
 
125
  return fastapi_app
126
 
127
 
 
50
  n_threads=4,
51
  verbose=False,
52
  )
 
 
 
 
 
 
 
 
 
53
  _llm_model = Llama(**kwargs)
54
  await llm_vol.commit.aio()
55
 
 
113
  },
114
  }
115
 
116
+ @fastapi_app.post("/warmup")
117
+ async def warmup(request: Request):
118
+ body = await request.json()
119
+ token = body.get("token", "")
120
+ expected = os.environ.get("EXPECTED_TOKEN", "")
121
+ if not token or token != expected:
122
+ raise HTTPException(401, "Unauthorized")
123
+ await _load_llm()
124
+ return {"status": "ready"}
125
+
126
  return fastapi_app
127
 
128