Mehdi commited on
Commit
dc3b7d4
·
1 Parent(s): 57ebbd6

docs: Field Notes blog post — what we built and what we learned

Browse files

Covers the full journey: MiniCPM4-8B on ZeroGPU, the Off-Brand
hidden-Gradio bridge (height:0, textbox triggers, polling), FLUX.2-klein
prefetching, client-side MCQ grading, and the double-handler bug.

Files changed (2) hide show
  1. BLOG.md +150 -0
  2. TASK.md +2 -2
BLOG.md ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 📄 PaperProf: How We Fought Gradio, Won, and Built an AI Study Buddy in 10 Days
2
+
3
+ *Field notes from the Build Small Hackathon — June 5–15, 2026*
4
+
5
+ ---
6
+
7
+ ## The Pitch
8
+
9
+ Every student knows the ritual: it's 11 PM, the exam is tomorrow, and you're re-reading the same lecture PDF for the fourth time, *feeling* productive while learning absolutely nothing. Passive re-reading is one of the worst-performing study techniques in the learning-science literature. Active recall — forcing yourself to answer questions — is one of the best.
10
+
11
+ So we built **PaperProf**: drop in any course PDF, and it becomes your personal professor. It reads the material, generates exam-style questions from it, grades your answers like a patient tutor, and even paints you a parting gift when you finish your session.
12
+
13
+ **[Try it live on Hugging Face Spaces →](https://huggingface.co/spaces/build-small-hackathon/PaperProf)**
14
+
15
+ Everything runs on free infrastructure with zero external API calls. No OpenAI key, no rate limits, no data leaving the machine. Just open-weight models doing honest work on a ZeroGPU slice.
16
+
17
+ ---
18
+
19
+ ## What It Does
20
+
21
+ 1. **Upload a PDF** — lecture notes, a textbook chapter, slides, whatever you're cramming.
22
+ 2. **PaperProf chunks it** into thematic sections and picks one at random.
23
+ 3. **Choose your mode:**
24
+ - **Open questions** — write a free-form answer, get structured tutor feedback: a verdict, what you got right, what you missed, and a model answer.
25
+ - **MCQ** — four plausible options, instant client-side grading, and a one-sentence explanation for *every* choice, not just the right one.
26
+ 4. **A score ring** tracks your session in real time.
27
+ 5. **End the session** and FLUX.2-klein generates a unique image inspired by the topics you just studied — a small visual reward for showing up.
28
+
29
+ The whole question-answer-feedback loop runs on **MiniCPM4-8B**, an open-weight 8B model, loaded once and shared between question generation and answer evaluation.
30
+
31
+ ```
32
+ PDF upload
33
+ └─► parser.py — PyMuPDF text extraction
34
+ └─► chunker.py — thematic chunking (min/max word caps)
35
+ └─► questioner.py — MiniCPM4-8B writes ONE focused question
36
+ └─► you answer
37
+ └─► evaluator.py — the same model grades you like a tutor
38
+ └─► image_gen.py — FLUX.2-klein paints your session
39
+ ```
40
+
41
+ ---
42
+
43
+ ## The Real Story: 68 Commits of Lessons
44
+
45
+ A hackathon README tells you what was built. The git log tells you what actually happened. Ours has 68 commits, and roughly two-thirds of them start with `fix:`. Here is the honest version.
46
+
47
+ ### Lesson 1 — Model choice is a compatibility problem, not a benchmark problem
48
+
49
+ We started with MiniCPM3-4B, upgraded to MiniCPM4-8B for better reasoning, and immediately hit the classic open-model trap: the model card says one thing, the `transformers` version on your machine says another.
50
+
51
+ ```
52
+ fix: pin transformers==4.57.1 for MiniCPM4-8B compatibility
53
+ ```
54
+
55
+ One pinned version later, everything worked. The follow-up lesson came from quantization: bitsandbytes 4-bit is great on a 16 GB local GPU and *completely unnecessary* on ZeroGPU's hardware — so we made it conditional:
56
+
57
+ ```python
58
+ # HF Spaces (ZeroGPU): skip quantization, use bfloat16 directly
59
+ if os.environ.get("SPACE_ID"):
60
+ return None
61
+ # Locally: 4-bit when VRAM < 17 GB
62
+ ```
63
+
64
+ Same code, two deployment targets, zero config files. Detect the environment, adapt.
65
+
66
+ ### Lesson 2 — The Off-Brand badge nearly broke us (and taught us the most)
67
+
68
+ The hackathon has an **Off-Brand** badge: ship a UI that doesn't look like the framework you built it with. We wanted PaperProf to look like a real product — glassmorphism, animated score ring, dark academia palette — not a Gradio demo.
69
+
70
+ Attempt #1: restyle Gradio with CSS. We fought the theme system through *eleven consecutive commits* (`fix: CSS labels illisibles`, `fix: override variables CSS Gradio`, `fix: retire primary_hue orange qui changeait toutes les teintes`...). Gradio's theming always had one more `!important` than we did.
71
+
72
+ Attempt #2: nuke it from orbit. Docker SDK, FastAPI serving raw HTML, Gradio relegated to a backend. It worked locally and died on Spaces — we lost ZeroGPU integration, which only flows through the Gradio SDK.
73
+
74
+ Attempt #3, the one that shipped: **the hidden-component bridge**. Keep Gradio as an invisible backend *inside the page*. Serve a fully custom HTML/CSS/JS interface through `gr.HTML`, hide every real Gradio component off-screen, and let a 300ms JavaScript polling loop ferry data between the two worlds.
75
+
76
+ This pattern produced the three hardest-won discoveries of the hackathon:
77
+
78
+ **`display: none` silently kills Gradio.** Components hidden that way never get their Svelte event handlers attached. The fix is the oldest trick in CSS:
79
+
80
+ ```css
81
+ /* collapsed but NOT display:none, so Gradio attaches event handlers */
82
+ #hidden-row-question { height: 0 !important; overflow: visible !important; }
83
+ ```
84
+
85
+ **You can't `.click()` a Gradio button from JS.** Server-side rendering means the synthetic click goes nowhere. What *does* work: programmatically setting a hidden textbox's value through the native property descriptor, then dispatching `input`/`change` events so Svelte notices:
86
+
87
+ ```javascript
88
+ function setGradioTA(sel, val) {
89
+ const el = document.querySelector(sel);
90
+ Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')
91
+ .set.call(el, val);
92
+ el.dispatchEvent(new Event('input', {bubbles: true}));
93
+ el.dispatchEvent(new Event('change', {bubbles: true}));
94
+ }
95
+ ```
96
+
97
+ Every action in PaperProf — generate question, submit answer, new MCQ — is a timestamp written into a hidden textbox, picked up by a `.change()` listener on the Python side. Buttons that aren't buttons.
98
+
99
+ **MutationObserver loses to Svelte.** Gradio's reactive DOM updates don't always fire observers the way you'd expect. We surrendered and switched to a humble `setInterval` polling loop. Less elegant, infinitely more reliable. Sometimes the dumb solution is the senior solution.
100
+
101
+ ### Lesson 3 — ZeroGPU makes you think in seconds
102
+
103
+ ZeroGPU gives you a serious GPU for free, but only in short decorated windows. That budget reshapes your architecture:
104
+
105
+ - **First-call cold starts are real.** Loading an 8B model takes ~60–90s the first time. We built the UI to be honest about it: a live elapsed-time counter, escalating messages ("Model loading…", "Still loading… first call can take ~90s"), and a 3-minute hard timeout that unlocks the UI instead of spinning forever.
106
+ - **Never download inside the GPU window.** FLUX.2-klein weighs ~16 GB. We prefetch it in a daemon thread at *startup*, so the `@spaces.GPU` window is spent generating, not downloading. We even skip a 7.75 GB duplicate ComfyUI checkpoint in the repo that diffusers never reads.
107
+ - **Don't burn GPU time on things JavaScript can do.** MCQ grading needs no model call — the LLM emits a structured format once (`QUESTION:` / `A)`–`D)` / `CORRECT:` / `EXPLAIN_A:`…), we parse it into JSON, and the browser grades clicks instantly. Zero latency, zero GPU seconds.
108
+
109
+ ### Lesson 4 — The bug that fired twice
110
+
111
+ Late in the hackathon, our session-summary modal showed every MCQ answer **duplicated**: answer one question, see it counted twice, score 0/2.
112
+
113
+ The cause was textbook event-handling: MCQ buttons had `btn.onclick = handler` assigned in the display function *and* an `addEventListener` registered by the global wiring function. One click, two handlers, two score increments. Our first fix removed the wrong one — the `addEventListener` had a timing flaw with its idempotency guard, so clicks then did *nothing at all*. The final fix kept the `onclick` (reassigned fresh with each question, inherently idempotent) and added a `mcqAnswered` re-entrancy guard for belt-and-suspenders.
114
+
115
+ Moral: when two pieces of code both "helpfully" wire the same button, you don't have redundancy — you have a race.
116
+
117
+ ### Lesson 5 — Prompts are product decisions
118
+
119
+ Small prompt details made the difference between "tech demo" and "usable study tool":
120
+
121
+ - Early questions were rambling multi-part monsters. The fix was brutal constraint: *"ONE question only, on ONE concept. Maximum 25 words. No sub-questions, no 'and'."*
122
+ - The evaluator follows a fixed 4-part structure (Verdict / What was good / What was missing / Model answer) so the frontend can parse and render it as styled sections — prompt format *is* API contract.
123
+ - With French source PDFs, the model kept drifting into French. Polite instructions lost to the gravitational pull of the context. What finally worked: `IMPORTANT: Always write in English, even if the source text is in another language` — stated twice, once at the top and once at the bottom of the prompt. With 8B models, subtlety is wasted; repetition is a feature.
124
+
125
+ ---
126
+
127
+ ## What We'd Tell Past Us
128
+
129
+ 1. **Read the git log of your own project sometimes.** Two-thirds `fix:` commits isn't failure — it's the actual texture of shipping. Each one was a lesson nobody had written down for us.
130
+ 2. **Frameworks fight back hardest at the edges.** Using Gradio normally is easy. Using it as an invisible backend required understanding how it *actually* renders. The weird workarounds (`height:0`, textbox triggers, polling) are now reusable knowledge.
131
+ 3. **Free infrastructure imposes honest engineering.** No API credits to hide behind means caring about cold starts, GPU seconds, and weight prefetching. Constraints made the architecture better.
132
+ 4. **Client-side everything you can.** The MCQ mode is the snappiest feature in the app precisely because it never touches the server after generation.
133
+ 5. **Ship the small thing.** PaperProf does one loop — read, ask, grade, encourage — and does it end-to-end. A hackathon project that completes one circle beats one that sketches five.
134
+
135
+ ---
136
+
137
+ ## The Stack
138
+
139
+ | Layer | Choice |
140
+ |---|---|
141
+ | Q&A + evaluation | MiniCPM4-8B (openbmb), bfloat16, transformers 4.57.1 |
142
+ | Session images | FLUX.2-klein-4B (Black Forest Labs), diffusers |
143
+ | PDF parsing | PyMuPDF |
144
+ | Backend / hosting | Gradio 6 on Hugging Face Spaces, ZeroGPU |
145
+ | Frontend | Hand-written HTML/CSS/JS over a hidden-Gradio bridge |
146
+ | External APIs | **None.** 🔌 Fully off the grid. |
147
+
148
+ ---
149
+
150
+ *Built for the Build Small Hackathon, June 2026. The Space is live — bring a PDF and let the professor grill you: [huggingface.co/spaces/build-small-hackathon/PaperProf](https://huggingface.co/spaces/build-small-hackathon/PaperProf)*
TASK.md CHANGED
@@ -57,7 +57,7 @@ Build Small Hackathon — June 5–15, 2026
57
  - [ ] Replace transformers pipeline with llama-cpp-python
58
  - [ ] Test performance vs transformers
59
  - [ ] 📡 Sharing is Caring: export and share agent trace on HuggingFace Hub
60
- - [ ] 📓 Field Notes: write blog post about what we built and learned
61
  - [ ] Document architecture decisions
62
  - [ ] Include benchmark results (speed, quality)
63
  - [ ] Publish on HuggingFace blog or personal blog
@@ -66,6 +66,6 @@ Build Small Hackathon — June 5–15, 2026
66
  - [ ] App running and stable on HuggingFace Space
67
  - [ ] Demo video (~2 minutes) showing full flow: upload → question → answer → feedback
68
  - [ ] Social media post (LinkedIn + Twitter) with Space link and demo
69
- - [ ] Blog post / Field Notes published
70
  - [ ] Submission form filled on HuggingFace
71
  - [ ] All badge requirements verified
 
57
  - [ ] Replace transformers pipeline with llama-cpp-python
58
  - [ ] Test performance vs transformers
59
  - [ ] 📡 Sharing is Caring: export and share agent trace on HuggingFace Hub
60
+ - [x] 📓 Field Notes: write blog post about what we built and learned (BLOG.md)
61
  - [ ] Document architecture decisions
62
  - [ ] Include benchmark results (speed, quality)
63
  - [ ] Publish on HuggingFace blog or personal blog
 
66
  - [ ] App running and stable on HuggingFace Space
67
  - [ ] Demo video (~2 minutes) showing full flow: upload → question → answer → feedback
68
  - [ ] Social media post (LinkedIn + Twitter) with Space link and demo
69
+ - [x] Blog post / Field Notes published (BLOG.md)
70
  - [ ] Submission form filled on HuggingFace
71
  - [ ] All badge requirements verified