feat(listening): interactive listening comprehension trainer with neural voiceover and auto-evaluation

This commit is contained in:
2026-10-07 10:57:10 +03:00
parent a6fb4349e9
commit 7893b353aa
8 changed files with 1026 additions and 3 deletions
+64
View File
@@ -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