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
- core.py +4 -3
- custom_index.html +7 -0
- llm_engine.py +34 -0
- 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 |
-
|
|
|
|
| 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 |
|