feat(listening): interactive listening comprehension trainer with neural voiceover and auto-evaluation
This commit is contained in:
+64
@@ -67,6 +67,11 @@ from app.tts import (
|
||||
AUDIO_CACHE_DIR,
|
||||
is_piper_available,
|
||||
)
|
||||
from app.listening import (
|
||||
get_listening_exercise,
|
||||
evaluate_listening_answer,
|
||||
prewarm_all_listening_audio,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +82,7 @@ async def lifespan(app: FastAPI):
|
||||
init_db()
|
||||
ensure_admin_user()
|
||||
asyncio.create_task(warmup_ollama_model())
|
||||
asyncio.create_task(prewarm_all_listening_audio())
|
||||
yield
|
||||
await close_http_client()
|
||||
|
||||
@@ -253,6 +259,13 @@ class MineBooksRequest(BaseModel):
|
||||
count: int = Field(15, ge=1, le=50)
|
||||
|
||||
|
||||
class ListeningCheckRequest(BaseModel):
|
||||
scenario_id: str
|
||||
user_answer: str
|
||||
story: Optional[str] = ""
|
||||
ideal_answer: Optional[str] = ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -540,6 +553,57 @@ async def api_tts_status(user: dict = Depends(require_user)):
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interactive Listening Comprehension Trainer (Stage 2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.get("/api/listening/exercise")
|
||||
async def api_listening_exercise(
|
||||
topic: Optional[str] = Query("all"),
|
||||
level: Optional[str] = Query("B1"),
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
"""
|
||||
Returns a listening comprehension exercise (topic / level / vocabulary context)
|
||||
with pre-synthesized audio URLs for the story and question.
|
||||
"""
|
||||
user_cards = []
|
||||
if topic == "cards":
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, term, translation, context_sentence
|
||||
FROM vocabulary_cards
|
||||
WHERE user_id = ? AND length(context_sentence) > 20
|
||||
ORDER BY RANDOM() LIMIT 20
|
||||
""",
|
||||
(user["id"],),
|
||||
).fetchall()
|
||||
user_cards = [dict(r) for r in rows]
|
||||
|
||||
ex = await get_listening_exercise(topic=topic, level=level, user_cards=user_cards)
|
||||
# Strip keywords and ideal_answer from client response to prevent inspection
|
||||
safe_ex = {k: v for k, v in ex.items() if k not in ("keywords", "ideal_answer")}
|
||||
return safe_ex
|
||||
|
||||
|
||||
@app.post("/api/listening/check")
|
||||
async def api_listening_check(
|
||||
req: ListeningCheckRequest,
|
||||
user: dict = Depends(require_user),
|
||||
):
|
||||
"""
|
||||
Evaluates user's answer to the listening question.
|
||||
Returns score, pedagogic feedback, full story transcription, and ideal answer.
|
||||
"""
|
||||
return evaluate_listening_answer(
|
||||
scenario_id=req.scenario_id,
|
||||
user_answer=req.user_answer,
|
||||
story=req.story or "",
|
||||
ideal_answer=req.ideal_answer or "",
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Collections
|
||||
|
||||
Reference in New Issue
Block a user