Ryadg commited on
Commit
53f316c
Β·
1 Parent(s): 8fd63cd

feat: FLUX.1-schnell image generation after correct answers

Browse files
Files changed (4) hide show
  1. app.py +65 -0
  2. core/image_gen.py +40 -0
  3. requirements.txt +3 -0
  4. ui/index.html +23 -0
app.py CHANGED
@@ -14,6 +14,7 @@ try:
14
  from core.chunker import chunk_text
15
  from core.questioner import generate_question, generate_mcq
16
  from core.evaluator import evaluate_answer
 
17
  print("βœ… All imports successful")
18
  except Exception as e:
19
  import traceback
@@ -170,6 +171,13 @@ body { background: #0A0F1E !important; margin: 0 !important; }
170
  pointer-events: none !important;
171
  }
172
 
 
 
 
 
 
 
 
173
  /* ── Load PDF button β€” defeat Gradio's orange primary override ──────────────── */
174
  .gr-button-primary,
175
  button.primary,
@@ -337,6 +345,29 @@ def submit_answer(question, chunk, student_answer, correct, total, language):
337
  return f"Erreur : {e}", correct, total, _score_label(correct, total)
338
 
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  # ---------------------------------------------------------------------------
341
  # UI
342
  # ---------------------------------------------------------------------------
@@ -353,6 +384,7 @@ BRIDGE_JS = """() => {
353
  mode: 'open',
354
  mcqData: null,
355
  waitingMCQ: false, prevMCQ: '',
 
356
  };
357
  const $ = id => document.getElementById(id);
358
 
@@ -456,6 +488,21 @@ BRIDGE_JS = """() => {
456
  return verdict;
457
  }
458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  // ── Question display ───────────────────────────────────────────────────────
460
  function showQuestion(text) {
461
  S.currentQuestion = text; S.waitingQ = false;
@@ -492,6 +539,7 @@ BRIDGE_JS = """() => {
492
  $('empty-state')?.classList.remove('hidden');
493
  $('question-area')?.classList.add('hidden');
494
  $('feedback-card')?.classList.add('hidden');
 
495
  }
496
 
497
  // ── MCQ display ────────────────────────────────────────────────────────────
@@ -572,6 +620,7 @@ BRIDGE_JS = """() => {
572
  if (es) es.classList.add('hidden');
573
  if (qa) qa.classList.remove('hidden');
574
  if (eb) eb.classList.remove('hidden');
 
575
  if (S.mode === 'mcq') {
576
  S.waitingMCQ = true;
577
  S.prevMCQ = document.querySelector('#hidden-mcq-output textarea')?.value || '';
@@ -642,6 +691,7 @@ BRIDGE_JS = """() => {
642
  if(ai){ai.value='';} if(cc) cc.textContent='0 chars'; if(sb) sb.disabled=true;
643
  if(fc) fc.classList.add('hidden'); if(es) es.classList.remove('hidden');
644
  if(qa) qa.classList.add('hidden'); if(eb) eb.classList.add('hidden');
 
645
  if(qc) qc.scrollIntoView({behavior:'smooth',block:'start'});
646
  }],
647
  ];
@@ -747,6 +797,16 @@ BRIDGE_JS = """() => {
747
  updateScore();
748
  }
749
  }
 
 
 
 
 
 
 
 
 
 
750
  }, 300);
751
  }"""
752
 
@@ -777,6 +837,7 @@ with gr.Blocks(title="PaperProf", css=CSS) as demo:
777
  answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", elem_id="hidden-answer-input")
778
  submit_btn = gr.Button("Submit Answer", variant="primary", elem_id="hidden-submit-btn")
779
  feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output")
 
780
  question_trigger = gr.Textbox(value="0", label="qtrig", elem_id="hidden-question-trigger")
781
  answer_trigger = gr.Textbox(value="0", label="atrig", elem_id="hidden-answer-trigger")
782
  chunk_display = gr.Textbox(value="", label="chunk", elem_id="hidden-chunk-output")
@@ -800,6 +861,10 @@ with gr.Blocks(title="PaperProf", css=CSS) as demo:
800
  submit_answer,
801
  inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_box],
802
  outputs=[feedback_box, correct_state, total_state, score_box],
 
 
 
 
803
  )
804
 
805
  mcq_trigger.change(
 
14
  from core.chunker import chunk_text
15
  from core.questioner import generate_question, generate_mcq
16
  from core.evaluator import evaluate_answer
17
+ from core.image_gen import generate_concept_image
18
  print("βœ… All imports successful")
19
  except Exception as e:
20
  import traceback
 
171
  pointer-events: none !important;
172
  }
173
 
174
+ /* ── Hidden concept image component ─────────────────────────────────────────── */
175
+ #hidden-concept-image {
176
+ position: fixed !important;
177
+ top: -9999px !important;
178
+ opacity: 0 !important;
179
+ }
180
+
181
  /* ── Load PDF button β€” defeat Gradio's orange primary override ──────────────── */
182
  .gr-button-primary,
183
  button.primary,
 
345
  return f"Erreur : {e}", correct, total, _score_label(correct, total)
346
 
347
 
348
+ def _is_positive_verdict(feedback: str) -> bool:
349
+ """True for Correct or Partially Correct verdicts (not Incorrect / errors)."""
350
+ for line in feedback.splitlines():
351
+ low = line.lower()
352
+ if "verdict" in low:
353
+ return "correct" in low and "incorrect" not in low
354
+ return False
355
+
356
+
357
+ @spaces.GPU(duration=60)
358
+ def generate_image_fn(question, feedback):
359
+ if not question or not _is_positive_verdict(feedback):
360
+ return None
361
+ try:
362
+ concept = question.strip().replace("\n", " ")[:200]
363
+ print(f"[generate_image_fn] generating illustration for: {concept[:80]!r}")
364
+ return generate_concept_image(concept)
365
+ except Exception as e:
366
+ import traceback; traceback.print_exc()
367
+ print(f"[generate_image_fn] failed: {e}")
368
+ return None
369
+
370
+
371
  # ---------------------------------------------------------------------------
372
  # UI
373
  # ---------------------------------------------------------------------------
 
384
  mode: 'open',
385
  mcqData: null,
386
  waitingMCQ: false, prevMCQ: '',
387
+ lastConceptImg: '',
388
  };
389
  const $ = id => document.getElementById(id);
390
 
 
488
  return verdict;
489
  }
490
 
491
+ // ── Concept image ──────────────────────────────────────────────────────────
492
+ function hideConceptImage() {
493
+ const wrap = $('concept-image-display');
494
+ if (wrap) { wrap.classList.add('hidden'); wrap.classList.remove('fade-in-up'); }
495
+ }
496
+
497
+ function showConceptImage(src) {
498
+ const wrap = $('concept-image-display'), img = $('concept-image');
499
+ if (!wrap || !img) return;
500
+ img.src = src;
501
+ wrap.classList.remove('hidden');
502
+ void wrap.offsetWidth; // restart fade-in animation
503
+ wrap.classList.add('fade-in-up');
504
+ }
505
+
506
  // ── Question display ───────────────────────────────────────────────────────
507
  function showQuestion(text) {
508
  S.currentQuestion = text; S.waitingQ = false;
 
539
  $('empty-state')?.classList.remove('hidden');
540
  $('question-area')?.classList.add('hidden');
541
  $('feedback-card')?.classList.add('hidden');
542
+ hideConceptImage();
543
  }
544
 
545
  // ── MCQ display ────────────────────────────────────────────────────────────
 
620
  if (es) es.classList.add('hidden');
621
  if (qa) qa.classList.remove('hidden');
622
  if (eb) eb.classList.remove('hidden');
623
+ hideConceptImage();
624
  if (S.mode === 'mcq') {
625
  S.waitingMCQ = true;
626
  S.prevMCQ = document.querySelector('#hidden-mcq-output textarea')?.value || '';
 
691
  if(ai){ai.value='';} if(cc) cc.textContent='0 chars'; if(sb) sb.disabled=true;
692
  if(fc) fc.classList.add('hidden'); if(es) es.classList.remove('hidden');
693
  if(qa) qa.classList.add('hidden'); if(eb) eb.classList.add('hidden');
694
+ hideConceptImage();
695
  if(qc) qc.scrollIntoView({behavior:'smooth',block:'start'});
696
  }],
697
  ];
 
797
  updateScore();
798
  }
799
  }
800
+
801
+ // Concept image: Gradio writes the generated image into the hidden
802
+ // gr.Image β€” surface it in the custom UI when a new src appears.
803
+ const conceptEl = document.querySelector('#hidden-concept-image img');
804
+ if (conceptEl && conceptEl.src && conceptEl.src !== S.lastConceptImg) {
805
+ S.lastConceptImg = conceptEl.src;
806
+ // Only show if feedback is still on screen (user hasn't moved on)
807
+ const fc = $('feedback-card');
808
+ if (fc && !fc.classList.contains('hidden')) showConceptImage(conceptEl.src);
809
+ }
810
  }, 300);
811
  }"""
812
 
 
837
  answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", elem_id="hidden-answer-input")
838
  submit_btn = gr.Button("Submit Answer", variant="primary", elem_id="hidden-submit-btn")
839
  feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output")
840
+ concept_image = gr.Image(label="concept", type="pil", interactive=False, visible=True, elem_id="hidden-concept-image")
841
  question_trigger = gr.Textbox(value="0", label="qtrig", elem_id="hidden-question-trigger")
842
  answer_trigger = gr.Textbox(value="0", label="atrig", elem_id="hidden-answer-trigger")
843
  chunk_display = gr.Textbox(value="", label="chunk", elem_id="hidden-chunk-output")
 
861
  submit_answer,
862
  inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_box],
863
  outputs=[feedback_box, correct_state, total_state, score_box],
864
+ ).then(
865
+ generate_image_fn,
866
+ inputs=[question_box, feedback_box],
867
+ outputs=[concept_image],
868
  )
869
 
870
  mcq_trigger.change(
core/image_gen.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Concept image generation via FLUX.1-schnell.
3
+ """
4
+
5
+ from functools import lru_cache
6
+
7
+ import torch
8
+ from PIL import Image
9
+
10
+
11
+ @lru_cache(maxsize=1)
12
+ def _load_pipeline():
13
+ # Imported lazily so the app can start (and CUDA stays untouched)
14
+ # outside the @spaces.GPU context that calls generate_concept_image.
15
+ from diffusers import FluxPipeline
16
+
17
+ pipe = FluxPipeline.from_pretrained(
18
+ "black-forest-labs/FLUX.1-schnell",
19
+ torch_dtype=torch.float16,
20
+ )
21
+ pipe.to("cuda")
22
+ return pipe
23
+
24
+
25
+ def generate_concept_image(concept: str) -> Image.Image:
26
+ pipe = _load_pipeline()
27
+ prompt = (
28
+ f"Educational illustration of: {concept}, "
29
+ "minimalist flat design, dark background, purple and cyan colors, "
30
+ "scientific diagram style"
31
+ )
32
+ result = pipe(
33
+ prompt,
34
+ height=512,
35
+ width=512,
36
+ num_inference_steps=4,
37
+ guidance_scale=0.0,
38
+ max_sequence_length=256,
39
+ )
40
+ return result.images[0]
requirements.txt CHANGED
@@ -10,6 +10,9 @@ spaces>=0.50.0
10
  # PDF extraction
11
  PyMuPDF>=1.24.0
12
 
 
 
 
13
  # LLM inference
14
  torch>=2.3.0
15
  transformers==4.57.1
 
10
  # PDF extraction
11
  PyMuPDF>=1.24.0
12
 
13
+ # Image generation
14
+ diffusers>=0.30.0
15
+
16
  # LLM inference
17
  torch>=2.3.0
18
  transformers==4.57.1
ui/index.html CHANGED
@@ -281,6 +281,22 @@
281
  .feedback-body .section-content { color: var(--text); }
282
  .feedback-raw { white-space: pre-wrap; }
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  /* ── Collapsible source ───────────────────────────────────────────────── */
285
  details.source-details { margin-top: 18px; }
286
  details.source-details summary {
@@ -534,6 +550,13 @@
534
  <div class="feedback-body" id="feedback-body"></div>
535
  <div class="mcq-explanations hidden" id="mcq-explanations"></div>
536
  </div>
 
 
 
 
 
 
 
537
  </div>
538
 
539
  <!-- ── Session modal ───────────────────────────────────────────────────── -->
 
281
  .feedback-body .section-content { color: var(--text); }
282
  .feedback-raw { white-space: pre-wrap; }
283
 
284
+ /* ── Concept image card ───────────────────────────────────────────────── */
285
+ #concept-image-display { text-align: center; }
286
+ .concept-image-title {
287
+ font-size: .72rem; font-weight: 700; letter-spacing: .1em;
288
+ text-transform: uppercase; color: var(--cyan-l);
289
+ margin-bottom: 14px; text-align: left;
290
+ }
291
+ #concept-image {
292
+ width: 100%; max-width: 512px; display: inline-block;
293
+ border-radius: var(--r-sm); border: 1px solid var(--border);
294
+ box-shadow: 0 8px 24px rgba(0,0,0,.4);
295
+ }
296
+ .concept-image-caption {
297
+ margin-top: 10px; font-size: .8rem; color: var(--muted);
298
+ }
299
+
300
  /* ── Collapsible source ───────────────────────────────────────────────── */
301
  details.source-details { margin-top: 18px; }
302
  details.source-details summary {
 
550
  <div class="feedback-body" id="feedback-body"></div>
551
  <div class="mcq-explanations hidden" id="mcq-explanations"></div>
552
  </div>
553
+
554
+ <!-- ── Concept image card ────────────────────────────────────────────── -->
555
+ <div class="card hidden" id="concept-image-display">
556
+ <div class="concept-image-title">Visual Summary</div>
557
+ <img id="concept-image" alt="Visual summary of this concept" />
558
+ <p class="concept-image-caption">Visual summary of this concept</p>
559
+ </div>
560
  </div>
561
 
562
  <!-- ── Session modal ───────────────────────────────────────────────────── -->