Mehdi commited on
Commit ·
7941f4d
1
Parent(s): bb37799
feat: thème éducatif natif Gradio + traduction UI FR/EN dynamique
Browse files
app.py
CHANGED
|
@@ -11,6 +11,47 @@ from core.chunker import chunk_text
|
|
| 11 |
from core.questioner import generate_question
|
| 12 |
from core.evaluator import evaluate_answer
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# ---------------------------------------------------------------------------
|
| 15 |
# State helpers
|
| 16 |
# ---------------------------------------------------------------------------
|
|
@@ -27,9 +68,24 @@ def _score_label(correct: int, total: int) -> str:
|
|
| 27 |
return f"✅ {correct} / {total}" if total > 0 else "—"
|
| 28 |
|
| 29 |
|
| 30 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
if pdf_file is None:
|
| 32 |
-
return [], "
|
| 33 |
try:
|
| 34 |
text = extract_text(pdf_file.name)
|
| 35 |
chunks = chunk_text(text)
|
|
@@ -38,14 +94,15 @@ def load_pdf(pdf_file):
|
|
| 38 |
except Exception as e:
|
| 39 |
return [], f"Erreur inattendue : {e}", 0, 0
|
| 40 |
if not chunks:
|
| 41 |
-
return [], "
|
| 42 |
-
return chunks,
|
| 43 |
|
| 44 |
|
| 45 |
@spaces.GPU(duration=60)
|
| 46 |
def new_question(chunks, language):
|
|
|
|
| 47 |
if not chunks:
|
| 48 |
-
return "", "", "
|
| 49 |
try:
|
| 50 |
chunk = random.choice(chunks)
|
| 51 |
question = generate_question(chunk, language=language)
|
|
@@ -56,10 +113,11 @@ def new_question(chunks, language):
|
|
| 56 |
|
| 57 |
@spaces.GPU(duration=120)
|
| 58 |
def submit_answer(question, chunk, student_answer, correct, total, language):
|
|
|
|
| 59 |
if not question:
|
| 60 |
-
return "
|
| 61 |
if not student_answer.strip():
|
| 62 |
-
return "
|
| 63 |
try:
|
| 64 |
feedback = evaluate_answer(question, chunk, student_answer, language=language)
|
| 65 |
total += 1
|
|
@@ -71,140 +129,126 @@ def submit_answer(question, chunk, student_answer, correct, total, language):
|
|
| 71 |
|
| 72 |
|
| 73 |
# ---------------------------------------------------------------------------
|
| 74 |
-
#
|
| 75 |
# ---------------------------------------------------------------------------
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
border-radius: 50px !important;
|
| 115 |
-
font-weight: 800 !important;
|
| 116 |
-
font-size: 1rem !important;
|
| 117 |
-
transition: all 0.15s ease !important;
|
| 118 |
-
}
|
| 119 |
-
button.primary { background: #F96A0A !important; color: #fff !important; border: none !important; }
|
| 120 |
-
button.primary:hover { background: #D95800 !important; transform: translateY(-1px) !important; box-shadow: 0 6px 14px rgba(249,106,10,0.3) !important; }
|
| 121 |
-
button.secondary { background: #3D3BD6 !important; color: #fff !important; border: none !important; }
|
| 122 |
-
button.secondary:hover { background: #2B29A8 !important; transform: translateY(-1px) !important; box-shadow: 0 6px 14px rgba(61,59,214,0.25) !important; }
|
| 123 |
-
|
| 124 |
-
/* Inputs */
|
| 125 |
-
textarea, input[type=text] {
|
| 126 |
-
border-radius: 12px !important;
|
| 127 |
-
border: 2px solid #E8DFB8 !important;
|
| 128 |
-
background: #FFFEF8 !important;
|
| 129 |
-
color: #1A1A2E !important;
|
| 130 |
-
font-size: 0.97rem !important;
|
| 131 |
-
}
|
| 132 |
-
textarea:focus { border-color: #F96A0A !important; box-shadow: 0 0 0 3px rgba(249,106,10,0.1) !important; }
|
| 133 |
|
| 134 |
-
/* Score */
|
| 135 |
#score-box textarea {
|
| 136 |
-
background: #3D3BD6 !important;
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
#status-box textarea { background: #EEEEFF !important; border-color: #3D3BD6 !important; color: #1A1A8E !important; font-weight: 600 !important; }
|
| 143 |
-
|
| 144 |
-
/* Question */
|
| 145 |
-
#question-box textarea { background: #FFF3E8 !important; border-color: #F96A0A !important; font-weight: 700 !important; }
|
| 146 |
-
|
| 147 |
-
/* Feedback */
|
| 148 |
#feedback-box textarea { background: #F0FFF6 !important; border-color: #00B08D !important; line-height: 1.75 !important; }
|
| 149 |
-
|
| 150 |
-
footer { display: none !important; }
|
| 151 |
"""
|
| 152 |
|
| 153 |
-
|
| 154 |
-
<div
|
| 155 |
-
<span style="font-size:
|
| 156 |
<div>
|
| 157 |
-
<div style="color:
|
| 158 |
-
|
| 159 |
-
</div>
|
| 160 |
-
<div style="color:rgba(255,255,255,0.82);font-size:1rem;font-family:Nunito,sans-serif;margin-top:5px">
|
| 161 |
-
Révise tes cours avec l'IA — questions, réponses, feedback instantané
|
| 162 |
-
</div>
|
| 163 |
</div>
|
| 164 |
</div>
|
| 165 |
"""
|
| 166 |
|
| 167 |
# ---------------------------------------------------------------------------
|
| 168 |
-
#
|
| 169 |
# ---------------------------------------------------------------------------
|
| 170 |
|
| 171 |
-
with gr.Blocks(title="PaperProf",
|
| 172 |
|
| 173 |
-
gr.HTML(
|
| 174 |
|
| 175 |
-
chunks_state
|
| 176 |
-
chunk_state
|
| 177 |
correct_state = gr.State(0)
|
| 178 |
-
total_state
|
| 179 |
|
| 180 |
with gr.Row():
|
| 181 |
-
pdf_input
|
| 182 |
-
load_btn
|
| 183 |
language_selector = gr.Radio(["English", "Français"], value="Français", label="🌐 Langue")
|
| 184 |
|
| 185 |
with gr.Row():
|
| 186 |
load_status = gr.Textbox(label="Statut", interactive=False, scale=4, elem_id="status-box")
|
| 187 |
-
score_box
|
| 188 |
|
| 189 |
with gr.Row():
|
| 190 |
question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="question-box")
|
| 191 |
-
new_q_btn
|
| 192 |
|
| 193 |
-
answer_box
|
| 194 |
-
submit_btn
|
| 195 |
feedback_box = gr.Textbox(label="💬 Feedback", interactive=False, lines=7, elem_id="feedback-box")
|
| 196 |
|
| 197 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
load_btn.click(
|
| 199 |
load_pdf,
|
| 200 |
-
inputs=[pdf_input],
|
| 201 |
outputs=[chunks_state, load_status, correct_state, total_state],
|
| 202 |
).then(lambda: "—", outputs=[score_box])
|
|
|
|
| 203 |
new_q_btn.click(
|
| 204 |
new_question,
|
| 205 |
inputs=[chunks_state, language_selector],
|
| 206 |
outputs=[question_box, chunk_state, feedback_box],
|
| 207 |
)
|
|
|
|
| 208 |
submit_btn.click(
|
| 209 |
submit_answer,
|
| 210 |
inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_selector],
|
|
|
|
| 11 |
from core.questioner import generate_question
|
| 12 |
from core.evaluator import evaluate_answer
|
| 13 |
|
| 14 |
+
# ---------------------------------------------------------------------------
|
| 15 |
+
# Internationalisation
|
| 16 |
+
# ---------------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
UI = {
|
| 19 |
+
"English": {
|
| 20 |
+
"load_btn": "Load PDF",
|
| 21 |
+
"new_q_btn": "New Question",
|
| 22 |
+
"submit_btn": "Submit Answer",
|
| 23 |
+
"pdf_label": "📎 Course PDF",
|
| 24 |
+
"status_label": "Status",
|
| 25 |
+
"question_label": "❓ Question",
|
| 26 |
+
"answer_label": "✏️ Your Answer",
|
| 27 |
+
"answer_ph": "Write your answer here…",
|
| 28 |
+
"feedback_label": "💬 Feedback",
|
| 29 |
+
"no_file": "No file selected.",
|
| 30 |
+
"no_chunks": "PDF loaded but no text found (scanned or too short?).",
|
| 31 |
+
"loaded": lambda n: f"📚 {n} sections extracted — ready to generate questions!",
|
| 32 |
+
"no_pdf": "⚠️ Please upload a PDF first.",
|
| 33 |
+
"no_question": "⚠️ Generate a question first.",
|
| 34 |
+
"no_answer": "⚠️ Please write an answer before submitting.",
|
| 35 |
+
},
|
| 36 |
+
"Français": {
|
| 37 |
+
"load_btn": "Charger le PDF",
|
| 38 |
+
"new_q_btn": "Nouvelle question",
|
| 39 |
+
"submit_btn": "Valider la réponse",
|
| 40 |
+
"pdf_label": "📎 Cours PDF",
|
| 41 |
+
"status_label": "Statut",
|
| 42 |
+
"question_label": "❓ Question",
|
| 43 |
+
"answer_label": "✏️ Ta réponse",
|
| 44 |
+
"answer_ph": "Écris ta réponse ici…",
|
| 45 |
+
"feedback_label": "💬 Feedback",
|
| 46 |
+
"no_file": "Aucun fichier sélectionné.",
|
| 47 |
+
"no_chunks": "PDF chargé mais aucun texte trouvé (scanné ou trop court ?).",
|
| 48 |
+
"loaded": lambda n: f"📚 {n} sections extraites — prêt à générer des questions !",
|
| 49 |
+
"no_pdf": "⚠️ Charge d'abord un PDF.",
|
| 50 |
+
"no_question": "⚠️ Génère d'abord une question.",
|
| 51 |
+
"no_answer": "⚠️ Écris une réponse avant de valider.",
|
| 52 |
+
},
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
# ---------------------------------------------------------------------------
|
| 56 |
# State helpers
|
| 57 |
# ---------------------------------------------------------------------------
|
|
|
|
| 68 |
return f"✅ {correct} / {total}" if total > 0 else "—"
|
| 69 |
|
| 70 |
|
| 71 |
+
def update_ui_language(language):
|
| 72 |
+
s = UI[language]
|
| 73 |
+
return (
|
| 74 |
+
gr.update(value=s["load_btn"]),
|
| 75 |
+
gr.update(value=s["new_q_btn"]),
|
| 76 |
+
gr.update(value=s["submit_btn"]),
|
| 77 |
+
gr.update(label=s["pdf_label"]),
|
| 78 |
+
gr.update(label=s["status_label"]),
|
| 79 |
+
gr.update(label=s["question_label"]),
|
| 80 |
+
gr.update(label=s["answer_label"], placeholder=s["answer_ph"]),
|
| 81 |
+
gr.update(label=s["feedback_label"]),
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def load_pdf(pdf_file, language):
|
| 86 |
+
s = UI[language]
|
| 87 |
if pdf_file is None:
|
| 88 |
+
return [], s["no_file"], 0, 0
|
| 89 |
try:
|
| 90 |
text = extract_text(pdf_file.name)
|
| 91 |
chunks = chunk_text(text)
|
|
|
|
| 94 |
except Exception as e:
|
| 95 |
return [], f"Erreur inattendue : {e}", 0, 0
|
| 96 |
if not chunks:
|
| 97 |
+
return [], s["no_chunks"], 0, 0
|
| 98 |
+
return chunks, s["loaded"](len(chunks)), 0, 0
|
| 99 |
|
| 100 |
|
| 101 |
@spaces.GPU(duration=60)
|
| 102 |
def new_question(chunks, language):
|
| 103 |
+
s = UI[language]
|
| 104 |
if not chunks:
|
| 105 |
+
return "", "", s["no_pdf"]
|
| 106 |
try:
|
| 107 |
chunk = random.choice(chunks)
|
| 108 |
question = generate_question(chunk, language=language)
|
|
|
|
| 113 |
|
| 114 |
@spaces.GPU(duration=120)
|
| 115 |
def submit_answer(question, chunk, student_answer, correct, total, language):
|
| 116 |
+
s = UI[language]
|
| 117 |
if not question:
|
| 118 |
+
return s["no_question"], correct, total, _score_label(correct, total)
|
| 119 |
if not student_answer.strip():
|
| 120 |
+
return s["no_answer"], correct, total, _score_label(correct, total)
|
| 121 |
try:
|
| 122 |
feedback = evaluate_answer(question, chunk, student_answer, language=language)
|
| 123 |
total += 1
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
# ---------------------------------------------------------------------------
|
| 132 |
+
# Thème éducatif
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
|
| 135 |
+
edu_theme = gr.themes.Base(
|
| 136 |
+
font=gr.themes.GoogleFont("Nunito"),
|
| 137 |
+
font_mono=gr.themes.GoogleFont("Nunito"),
|
| 138 |
+
).set(
|
| 139 |
+
# Fond général
|
| 140 |
+
body_background_fill="#FFF8DC",
|
| 141 |
+
body_text_color="#1A1A2E",
|
| 142 |
+
# Cards
|
| 143 |
+
block_background_fill="#FFFFFF",
|
| 144 |
+
block_border_color="#F0E0A0",
|
| 145 |
+
block_border_width="2px",
|
| 146 |
+
block_radius="18px",
|
| 147 |
+
block_shadow="0 2px 16px rgba(0,0,0,0.07)",
|
| 148 |
+
block_label_text_color="#3D3BD6",
|
| 149 |
+
block_label_text_size="sm",
|
| 150 |
+
block_label_text_weight="700",
|
| 151 |
+
# Inputs
|
| 152 |
+
input_background_fill="#FFFEF5",
|
| 153 |
+
input_border_color="#F0E0A0",
|
| 154 |
+
input_radius="12px",
|
| 155 |
+
# Boutons primary (orange)
|
| 156 |
+
button_primary_background_fill="#F96A0A",
|
| 157 |
+
button_primary_background_fill_hover="#D95800",
|
| 158 |
+
button_primary_text_color="#FFFFFF",
|
| 159 |
+
button_primary_border_color="#F96A0A",
|
| 160 |
+
button_large_radius="50px",
|
| 161 |
+
button_large_padding="12px 28px",
|
| 162 |
+
button_large_text_weight="800",
|
| 163 |
+
button_large_text_size="md",
|
| 164 |
+
# Boutons secondary (bleu)
|
| 165 |
+
button_secondary_background_fill="#3D3BD6",
|
| 166 |
+
button_secondary_background_fill_hover="#2B29A8",
|
| 167 |
+
button_secondary_text_color="#FFFFFF",
|
| 168 |
+
button_secondary_border_color="#3D3BD6",
|
| 169 |
+
# Panel
|
| 170 |
+
panel_background_fill="#FFFFFF",
|
| 171 |
+
panel_border_color="#F0E0A0",
|
| 172 |
+
# Slider / Radio
|
| 173 |
+
color_accent="#F96A0A",
|
| 174 |
+
color_accent_soft="#FFF3E8",
|
| 175 |
+
)
|
| 176 |
|
| 177 |
+
CSS = """
|
| 178 |
+
footer { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
|
|
|
| 180 |
#score-box textarea {
|
| 181 |
+
background: #3D3BD6 !important;
|
| 182 |
+
color: #fff !important;
|
| 183 |
+
font-weight: 900 !important;
|
| 184 |
+
font-size: 1.4rem !important;
|
| 185 |
+
text-align: center !important;
|
| 186 |
+
border: none !important;
|
| 187 |
}
|
| 188 |
+
#status-box textarea { background: #EEF0FF !important; color: #2B29A8 !important; font-weight: 600 !important; }
|
| 189 |
+
#question-box textarea { background: #FFF3E8 !important; border-color: #F96A0A !important; font-weight: 700 !important; font-size: 1.05rem !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
#feedback-box textarea { background: #F0FFF6 !important; border-color: #00B08D !important; line-height: 1.75 !important; }
|
|
|
|
|
|
|
| 191 |
"""
|
| 192 |
|
| 193 |
+
HEADER = """
|
| 194 |
+
<div style="background:linear-gradient(135deg,#3D3BD6,#6B69FF);border-radius:20px;padding:24px 28px;margin-bottom:4px;display:flex;align-items:center;gap:18px">
|
| 195 |
+
<span style="font-size:2.8rem">📄</span>
|
| 196 |
<div>
|
| 197 |
+
<div style="color:#fff;font-size:1.9rem;font-weight:900;font-family:Nunito,sans-serif;line-height:1.1">PaperProf</div>
|
| 198 |
+
<div style="color:rgba(255,255,255,.82);font-size:0.95rem;font-family:Nunito,sans-serif;margin-top:4px">Révise tes cours avec l'IA · questions · réponses · feedback instantané</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</div>
|
| 200 |
</div>
|
| 201 |
"""
|
| 202 |
|
| 203 |
# ---------------------------------------------------------------------------
|
| 204 |
+
# UI
|
| 205 |
# ---------------------------------------------------------------------------
|
| 206 |
|
| 207 |
+
with gr.Blocks(title="PaperProf", theme=edu_theme, css=CSS) as demo:
|
| 208 |
|
| 209 |
+
gr.HTML(HEADER)
|
| 210 |
|
| 211 |
+
chunks_state = gr.State([])
|
| 212 |
+
chunk_state = gr.State("")
|
| 213 |
correct_state = gr.State(0)
|
| 214 |
+
total_state = gr.State(0)
|
| 215 |
|
| 216 |
with gr.Row():
|
| 217 |
+
pdf_input = gr.File(label="📎 Cours PDF", file_types=[".pdf"], scale=3)
|
| 218 |
+
load_btn = gr.Button("Charger le PDF", variant="primary", scale=2)
|
| 219 |
language_selector = gr.Radio(["English", "Français"], value="Français", label="🌐 Langue")
|
| 220 |
|
| 221 |
with gr.Row():
|
| 222 |
load_status = gr.Textbox(label="Statut", interactive=False, scale=4, elem_id="status-box")
|
| 223 |
+
score_box = gr.Textbox(label="Score", value="—", interactive=False, scale=1, elem_id="score-box")
|
| 224 |
|
| 225 |
with gr.Row():
|
| 226 |
question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="question-box")
|
| 227 |
+
new_q_btn = gr.Button("Nouvelle question", variant="secondary", scale=1)
|
| 228 |
|
| 229 |
+
answer_box = gr.Textbox(label="✏️ Ta réponse", lines=4, placeholder="Écris ta réponse ici…")
|
| 230 |
+
submit_btn = gr.Button("Valider la réponse", variant="primary")
|
| 231 |
feedback_box = gr.Textbox(label="💬 Feedback", interactive=False, lines=7, elem_id="feedback-box")
|
| 232 |
|
| 233 |
+
# Traduction UI quand on change la langue
|
| 234 |
+
language_selector.change(
|
| 235 |
+
update_ui_language,
|
| 236 |
+
inputs=[language_selector],
|
| 237 |
+
outputs=[load_btn, new_q_btn, submit_btn, pdf_input, load_status, question_box, answer_box, feedback_box],
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
load_btn.click(
|
| 241 |
load_pdf,
|
| 242 |
+
inputs=[pdf_input, language_selector],
|
| 243 |
outputs=[chunks_state, load_status, correct_state, total_state],
|
| 244 |
).then(lambda: "—", outputs=[score_box])
|
| 245 |
+
|
| 246 |
new_q_btn.click(
|
| 247 |
new_question,
|
| 248 |
inputs=[chunks_state, language_selector],
|
| 249 |
outputs=[question_box, chunk_state, feedback_box],
|
| 250 |
)
|
| 251 |
+
|
| 252 |
submit_btn.click(
|
| 253 |
submit_answer,
|
| 254 |
inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_selector],
|