Spaces:
Running
A newer version of the Gradio SDK is available: 6.20.0
French Coach — Custom UI API contract
All endpoints are mounted on the same Gradio app object (gr.Server /
FastAPI instance) returned by demo.launch(prevent_thread_lock=True) in
app_custom.py, under the /api/... prefix. The built React app
(frontend/dist) is served as static assets from the same server, so the
whole thing is one Gradio-SDK process (no separate API host).
User identity: single-user dev mode. Every endpoint operates on a fixed
USER_ID = "dev_user" (same default app.py uses when not running on a
Space). Hugging Face OAuth for the custom UI on the public Space is not
wired up yet — flagged as a follow-up, not a blocker (does not affect
Gradio-SDK / model-size eligibility).
All responses are JSON unless noted. Errors: {"error": "<message>"} with a
non-200 status code.
Lessons (Notebook + Lessons browser)
GET /api/lessons
List saved lesson pages (excludes page_type == "resource"), newest first.
Response:
{
"lessons": [
{"id": "uuid", "title": "Class 4 — Le passé composé", "date": "2026-04-30",
"category": "Grammar", "page_type": "lesson", "preview": "Aujourd'hui on a vu…"}
]
}
GET /api/lessons/{id}
Load one page for editing.
Response:
{"id": "uuid", "title": "Class 4", "raw_text": "Le petit chat...",
"annotations": {"tokens": [...], "meanings": {...}}}
404 -> {"error": "not found"} if missing or not owned by user.
POST /api/lessons
Save the current editor text as a new page (curator auto-titles it).
Awards saved_lesson points.
Body: {"text": "...", "annotations": {...}}
Response: {"id": "uuid", "title": "Auto-generated title"}
PUT /api/lessons/{id}
Update an existing page's text/annotations in place (title unchanged).
Body: {"text": "...", "annotations": {...}}
Response: {"title": "Existing title"}
PATCH /api/lessons/{id}/title
Rename a page (user override of the auto title).
Body: {"title": "New title"}
Response: {"title": "New title"}
DELETE /api/lessons/{id}
Delete a page (exercises cascade).
Response: {"deleted": true}
Resources tab
GET /api/resources
Pages curated as page_type == "resource" that contain links and/or books.
Response:
{
"resources": [
{"id": "uuid", "title": "Online Resources",
"links": [{"url": "https://...", "label": "TV5 Monde", "domain": "tv5monde.com"}],
"books": [{"title": "Le Petit Prince", "author": "Saint-Exupéry", "note": "easy reader"}]}
]
}
Annotation / gender coloring / word card (Notebook + Tools screens)
POST /api/annotate
Run spaCy annotation on arbitrary text (used by Notebook "Annotate" and the Gender Checker tool).
Body: {"text": "...", "colors_on": true}
Response:
{"html": "<span data-token=\"1\" data-gender=\"Masc\" ...>Le</span> ...",
"tokens": [{"text": "Le", "lemma": "le", "pos": "DET", "gender": "Masc", "whitespace": " "}],
"meanings": {}}
html is the same gender-colored markup nlp.render_html already produces
(with data-token/data-text/data-gender/data-pos/data-lemma
attributes) — the React Notebook/Tools screens render it with
dangerouslySetInnerHTML and use click-event delegation, exactly like the
existing Blocks PAGE_JS, so gender colors + click behavior stay identical.
POST /api/render
Re-render cached annotations to gender-colored HTML without re-running
spaCy/LLM (used when loading a saved lesson, or toggling the gender-colors
checkbox, so cached meanings survive).
Body: {"annotations": {"tokens": [...], "meanings": {...}}, "colors_on": true}
Response: {"html": "<span data-token=...>...</span> ..."}
POST /api/word-card
Get (and cache) the LLM meaning/grammar note for one clicked word. Awards
word_explored points the first time a given lemma is looked up.
Body:
{"text": "femme", "lemma": "femme", "pos": "NOUN", "gender": "Fem",
"meanings": {"...cached meanings dict from annotate/lessons..."}}
Response:
{"text": "femme", "lemma": "femme", "pos": "NOUN", "gender": "Fem",
"meaning": "woman", "grammar": "feminine noun",
"meanings": {"femme": {"meaning": "woman", "grammar": "feminine noun"}, "...": "..."}}
The client merges the returned meanings dict back into its local
annotations object (so a later "Save"/"Update" persists the cache).
Chat coach
POST /api/chat
Non-streaming reply (the custom UI does one round trip per message instead of token-streaming).
Body:
{"message": "Comment dit-on 'thank you'?",
"history": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}],
"lesson_text": "...current notebook text, used as context, optional..."}
Response: {"reply": "On dit « merci »..."}
Exercises
POST /api/exercises/coach
Coach Agent (Day 3): plans a balanced 5-7 item practice set from the
current lesson, grounded against the A1/A2 CEFR syllabus
(syllabus_full_a1_c2.json), generates each item, critiques it, and
regenerates anything that fails review (max 2 attempts/item). Identified
concepts are upserted into concepts (covered_on = today) for the Summary
tab's "next focus".
Body: {"lesson_text": "...", "page_id": "uuid (optional)"}
Response:
{
"concepts": [{"id": "verb_etre_present", "name": "Verb: Être (Present Tense)", "cefr_level": "A1", "family": "verb_tenses"}],
"exercises": [
{"type": "fill_blank", "instruction": "Fill in the blank:", "sentence_with_blank": "...", "answer": "...", "hint": "...", "explanation": "..."},
{"type": "multiple_choice", "instruction": "Choose the correct answer:", "question": "...", "options": ["...", "...", "...", "..."], "answer": "...", "explanation": "..."},
{"type": "error_detection", "instruction": "Find and fix the mistake:", "sentence": "...", "answer": "...", "explanation": "..."},
{"type": "reorder", "instruction": "Put the words in the correct order:", "words": ["...", "..."], "answer": "...", "explanation": "..."},
{"type": "translation", "instruction": "Translate to French:", "prompt": "...", "answer": "...", "explanation": "..."}
]
}
The frontend shows exercises one at a time (see Exercises.jsx
CoachExercises).
POST /api/exercises/coach/check
Check one item's answer. fill_blank/multiple_choice are checked by exact
match; error_detection/reorder/translation are graded leniently by the
LLM (accepts spelling/accent/punctuation variation). Always awards
exercise_done points — participation, not correctness; never red/shaming.
Body: {"exercise": {...}, "answer": "..."}
Response: {"correct": true, "feedback": "encouraging message", "answer": "model answer"}
POST /api/exercises/dialogue
Start a new dialogue scene from the lesson.
Body: {"lesson_text": "..."}
Response:
{"dialogue": {"scene": "...", "agent_role": "...", "user_role": "...", "turns": [...]},
"replies": [], "hint": "Your turn: ...", "transcript_html": "<div ...>...</div>"}
POST /api/exercises/dialogue/reply
Send the user's next line. Awards dialogue_turn points.
Body: {"dialogue": {...}, "replies": ["..."], "reply": "Bonjour !"}
Response:
{"replies": ["Bonjour !"], "transcript_html": "<div ...>...</div>",
"hint": "Your turn: ..." , "feedback_html": "<div ...>...</div>"}
(hint is "🎉 Dialogue complete! Great work!" once finished.)
POST /api/exercises/visual/sample
Matched-image visual exercise (Day 4): no upload needed. Picks one of ~15
pre-generated images (frontend/public/sample_images/, generated once via
generate_sample_images.py) matching the lesson's detected topic
(nlp.detect_category), avoiding images this user has already seen
(user_image_usage) until the set cycles. Builds 3-5 exercises (with hints)
grounded in the image's hand-written description — no vision call at request
time. Awards photo_exercise points.
Body: {"lesson_text": "..."}
Response:
{
"image_url": "/custom/sample_images/food_dining.jpg",
"topic": "Food & Dining",
"html": "<div ...>...</div>"
}
(html = exercises.render_visual_exercises(result), includes a hint line
per exercise.)
POST /api/exercises/pronunciation/target
Body: {"lesson_text": "..."}
Response:
{"target": {"phrase": "...", "translation": "...", "tip": "..."}, "html": "<div ...>...</div>"}
POST /api/exercises/pronunciation/check
Awards pronunciation points.
Body: {"target": {"phrase": "..."}, "transcription": "..."}
Response: {"html": "<div ...>...</div>"}
Gender Checker + Translator (Tools)
POST /api/gender-check
Look up a single French noun. spaCy (nlp.word_info) gives a
lemma/POS hint, but gender itself is not reliable from spaCy on an
isolated word (no determiner context to disambiguate — e.g. "pomme" alone
tags Masc though it's feminine), so the LLM is authoritative for
gender/articles/example/pattern note.
Body: {"word": "pomme"}
Response:
{
"word": "pomme", "lemma": "pomme", "pos": "NOUN",
"gender": "Fem", "article": "la", "indefinite_article": "une",
"example": "J'achète une pomme pour le goûter.",
"example_translation": "I'm buying an apple for the snack.",
"pattern_note": "Words ending in -e are often feminine."
}
POST /api/translate
Translate a word/phrase with alternatives and a bilingual example. If
lesson_text is given, it's used as register/vocabulary context only.
Body: {"text": "good morning", "direction": "en_fr"|"fr_en", "lesson_text": "..."}
Response:
{
"translation": "bonjour",
"alternatives": [],
"example_fr": "Bonjour, comment allez-vous ?",
"example_en": "Good morning, how are you?"
}
example_fr/example_en are always in their named language regardless of
direction (the LLM was inconsistent about which side of example/
example_translation was French, so the schema is now language-explicit).
Summary / gamification
GET /api/summary
Calls gamify.try_daily_open (idempotent per day, awards daily_open
points once/day) then returns the encouraging daily summary, total points,
today's activity stats, and A1-A2 concept progress for the dashboard.
Response:
{
"summary": "You've covered 6 concepts...",
"total_points": 142,
"daily_stats": {
"pages_today": 1, "exercises_today": 5,
"dialogue_turns": 0, "words_clicked": 3, "total_points": 142
},
"concepts": {
"covered": ["Personal Subject Pronouns", "Regular -ER Verbs (Present)"],
"next": "French Pronunciation & Sound System",
"covered_count": 8, "total_count": 49
}
}
Screens -> endpoints map (for Phase 3 ordering)
- Notebook —
/api/lessons/{id}(load),/api/annotate,/api/word-card,/api/lessons(save new),/api/lessons/{id}PUT (update),/api/lessons/{id}/titlePATCH (rename),/api/lessons/{id}DELETE. - Lessons browser —
/api/lessons(list, grouped client-side by date/category). - Exercises — the four
/api/exercises/...groups. - Chat coach —
/api/chat. - Summary dashboard —
/api/summary. - Tools —
/api/annotate//api/word-card(Text Checker),/api/gender-check(Gender Checker),/api/translate(Translator) — all standalone utilities, separate from the saved notebook.