split cards

This commit is contained in:
2026-05-01 21:12:05 +02:00
parent 214dd0b6a6
commit 2b65876525
6 changed files with 171 additions and 25 deletions

View File

@@ -16,7 +16,14 @@ from .auth import (
verify_password,
)
from .curriculum import QUESTIONS
from .program_content import decode_asset_token, get_content_status, get_lesson, list_lessons_for_grade, normalize_grade
from .program_content import (
decode_asset_token,
get_content_status,
get_lesson,
list_lessons_for_grade,
normalize_grade,
render_svg_section,
)
from .services import (
build_llm_reply,
ensure_student_mastery,
@@ -214,6 +221,22 @@ def get_program_asset(
return FileResponse(path, media_type="image/svg+xml")
@app.get("/program/assets/{asset_token}/sections/{section_index}")
def get_program_asset_section(
asset_token: str,
section_index: int,
current_user: models.User = Depends(get_current_user),
):
try:
path = decode_asset_token(asset_token)
svg = render_svg_section(path, section_index)
except IndexError as exc:
raise HTTPException(status_code=404, detail="Étape introuvable") from exc
except Exception as exc:
raise HTTPException(status_code=400, detail="Ressource invalide") from exc
return Response(content=svg, media_type="image/svg+xml")
@app.get("/students/{student_id}/program", response_model=schemas.StudentProgramResponse)
def get_student_program(
student_id: int,

View File

@@ -1,5 +1,7 @@
import base64
import html
import os
import re
from functools import lru_cache
from pathlib import Path
from urllib.parse import quote
@@ -72,6 +74,61 @@ def build_asset_url(relative_path: str) -> str:
return f"/api/program/assets/{quote(encode_asset_path(relative_path))}"
def build_section_url(relative_path: str, section_index: int) -> str:
token = quote(encode_asset_path(relative_path))
return f"/api/program/assets/{token}/sections/{section_index}"
def extract_svg_sections(svg_path: Path) -> list[dict]:
text = svg_path.read_text(encoding="utf-8", errors="ignore")
sections: list[dict] = []
pattern = re.compile(
r'<g\s+transform="translate\((?P<x>[-\d.]+)\s+(?P<y>[-\d.]+)\)">\s*'
r'<rect\s+x="0"\s+y="0"\s+width="(?P<w>[-\d.]+)"\s+height="(?P<h>[-\d.]+)"[^>]*>'
r'(?P<body>.*?)</g>',
re.DOTALL,
)
for match in pattern.finditer(text):
width = float(match.group("w"))
height = float(match.group("h"))
if width < 250 or height < 120:
continue
title_match = re.search(r'<text[^>]*>(?P<title>.*?)</text>', match.group("body"), re.DOTALL)
raw_title = re.sub(r"<[^>]+>", "", title_match.group("title") if title_match else "")
title = html.unescape(raw_title).strip() or f"Étape {len(sections) + 1}"
margin = 28
sections.append(
{
"index": len(sections),
"title": title,
"view_box": [
max(float(match.group("x")) - margin, 0),
max(float(match.group("y")) - margin, 0),
width + margin * 2,
height + margin * 2,
],
}
)
return sections
def render_svg_section(path: Path, section_index: int) -> str:
sections = extract_svg_sections(path)
if section_index < 0 or section_index >= len(sections):
raise IndexError("Section introuvable")
x, y, width, height = sections[section_index]["view_box"]
text = path.read_text(encoding="utf-8", errors="ignore")
text = re.sub(r'\swidth="[^"]+"', f' width="{int(width)}"', text, count=1)
text = re.sub(r'\sheight="[^"]+"', f' height="{int(height)}"', text, count=1)
text = re.sub(
r'\sviewBox="[^"]+"',
f' viewBox="{x:g} {y:g} {width:g} {height:g}"',
text,
count=1,
)
return text
@lru_cache(maxsize=1)
def list_lessons() -> list[dict]:
lessons: list[dict] = []
@@ -106,15 +163,25 @@ def list_lessons() -> list[dict]:
)
title = first_heading or title
assets = [
{
"name": svg_path.stem,
"title": title_from_slug(svg_path.stem),
"path": svg_path.relative_to(CONTENT_ROOT).as_posix(),
"url": build_asset_url(svg_path.relative_to(CONTENT_ROOT).as_posix()),
}
for svg_path in svg_files
]
assets = []
for svg_path in svg_files:
relative_path = svg_path.relative_to(CONTENT_ROOT).as_posix()
sections = extract_svg_sections(svg_path)
assets.append(
{
"name": svg_path.stem,
"title": title_from_slug(svg_path.stem),
"path": relative_path,
"url": build_asset_url(relative_path),
"sections": [
{
**section,
"url": build_section_url(relative_path, section["index"]),
}
for section in sections
],
}
)
lessons.append(
{

View File

@@ -106,6 +106,7 @@ class ProgramAsset(BaseModel):
title: str
path: str
url: str
sections: List[dict] = []
class ProgramLesson(BaseModel):

View File

@@ -131,6 +131,11 @@ def get_student_context(db: Session, student_id: int) -> str:
lines.append(
f"Leçon programme sélectionnée par le professeur: {lesson['title']} ({lesson['subject']}, {lesson['grade']})."
)
first_asset = (lesson.get("assets") or [None])[0]
if first_asset:
section_titles = [section["title"] for section in first_asset.get("sections", [])]
if section_titles:
lines.append("Étapes de la première fiche: " + ", ".join(section_titles) + ".")
for mastery_row, skill in mastery:
lines.append(
f"- {skill.label}: score={mastery_row.mastery_score:.1f}, confiance={mastery_row.confidence:.2f}, preuves={mastery_row.evidence_count}"